Compare commits

...
15 Commits
Author SHA1 Message Date
alam00000 0d9ffe2997 Release v2.4.1 2026-03-07 14:38:08 +05:30
alam00000 d32251014e feat: integrate fix page size functionality into the workflow by extracting core logic to a utility and adding a new workflow node. 2026-03-07 14:33:33 +05:30
alam00000 c2147a2f20 Refactor page titles and subtitles for consistency and clarity across various PDF conversion tools 2026-03-04 13:46:16 +05:30
alam00000 90998edb25 Add tests for image effects, PDF operations, rotation state, and XML to PDF utilities 2026-03-04 13:33:00 +05:30
alam00000 84848ab902 feat: add Quick Look page preview and unify thumbnail styles across all tools 2026-03-04 00:38:07 +05:30
alam00000 2aaea50031 feat: fix bug for remove blank pages tool. added i18n translations 2026-03-03 23:35:15 +05:30
Alam 5fbf48601e Merge pull request #535 from Stephan-P/patch-1
Update tools.json - Option Apply watermark to all pages
2026-03-02 21:38:45 +05:30
github-actions[bot] 4cec7bbe58 @InstaZDLL has signed the CLA in alam00000/bentopdf#538 2026-03-02 14:33:23 +00:00
github-actions[bot] e505c70b2f @Olivetti has signed the CLA in alam00000/bentopdf#531 2026-03-02 13:46:13 +00:00
Stephan Paternotte 841428f2e2 Update tools.json - Option Apply watermark to all pages
Option Apply watermark to all pages TRANSLATED
2026-03-02 07:03:06 +01:00
alam00000 957b78fafd fix: revert cropperjs dependency to version 1.6.2 and adjust Docker build commands for CORS proxy secrets 2026-03-01 21:29:13 +05:30
Alam 34f12655c1 Merge pull request #534 from alam00000/dependabot/npm_and_yarn/npm-dependencies-b1e467bd1f
build(deps): bump the npm-dependencies group across 1 directory with 29 updates
2026-03-01 19:59:30 +05:30
Alam 74d7af283e Merge pull request #533 from alam00000/dependabot/npm_and_yarn/svelte-5.53.6
build(deps): bump svelte from 5.53.1 to 5.53.6
2026-03-01 19:57:44 +05:30
dependabot[bot] 5ef73edea2 build(deps): bump the npm-dependencies group across 1 directory with 29 updates
Bumps the npm-dependencies group with 28 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@kenjiuno/msgreader](https://github.com/HiraokaHyperTools/msgreader) | `1.27.1-alpha.1` | `1.28.0` |
| [@matbee/libreoffice-converter](https://github.com/matbeedotcom/libreoffice-document-converter) | `2.3.1` | `2.5.0` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.1.18` | `4.2.1` |
| [cropperjs](https://github.com/fengyuanchen/cropperjs/tree/HEAD/packages/cropperjs) | `1.6.2` | `2.1.0` |
| [i18next](https://github.com/i18next/i18next) | `25.7.3` | `25.8.13` |
| [i18next-browser-languagedetector](https://github.com/i18next/i18next-browser-languageDetector) | `8.2.0` | `8.2.1` |
| [lucide](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide) | `0.546.0` | `0.575.0` |
| [mermaid](https://github.com/mermaid-js/mermaid) | `11.12.2` | `11.12.3` |
| [pdfjs-dist](https://github.com/mozilla/pdf.js) | `5.4.530` | `5.4.624` |
| [postal-mime](https://github.com/postalsys/postal-mime) | `2.7.1` | `2.7.3` |
| [sortablejs](https://github.com/SortableJS/Sortable) | `1.15.6` | `1.15.7` |
| [terser](https://github.com/terser/terser) | `5.44.1` | `5.46.0` |
| [tesseract.js](https://github.com/naptha/tesseract.js) | `6.0.1` | `7.0.0` |
| [vite-plugin-static-copy](https://github.com/sapphi-red/vite-plugin-static-copy) | `3.1.4` | `3.2.0` |
| [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.39.2` | `10.0.1` |
| [@types/pdfkit](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/pdfkit) | `0.17.4` | `0.17.5` |
| [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) | `3.2.4` | `4.0.18` |
| [@vitest/ui](https://github.com/vitest-dev/vitest/tree/HEAD/packages/ui) | `3.2.4` | `4.0.18` |
| [eslint](https://github.com/eslint/eslint) | `9.39.2` | `10.0.2` |
| [globals](https://github.com/sindresorhus/globals) | `17.0.0` | `17.4.0` |
| [jsdom](https://github.com/jsdom/jsdom) | `27.4.0` | `28.1.0` |
| [lint-staged](https://github.com/lint-staged/lint-staged) | `16.2.7` | `16.3.1` |
| [prettier](https://github.com/prettier/prettier) | `3.7.4` | `3.8.1` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.51.0` | `8.56.1` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.3.0` | `7.3.1` |
| [vite-plugin-node-polyfills](https://github.com/davidmyersdev/vite-plugin-node-polyfills) | `0.24.0` | `0.25.0` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `3.2.4` | `4.0.18` |
| [vue](https://github.com/vuejs/core) | `3.5.26` | `3.5.29` |



Updates `@kenjiuno/msgreader` from 1.27.1-alpha.1 to 1.28.0
- [Commits](https://github.com/HiraokaHyperTools/msgreader/commits)

Updates `@matbee/libreoffice-converter` from 2.3.1 to 2.5.0
- [Release notes](https://github.com/matbeedotcom/libreoffice-document-converter/releases)
- [Changelog](https://github.com/matbeedotcom/libreoffice-document-converter/blob/main/CHANGELOG.md)
- [Commits](https://github.com/matbeedotcom/libreoffice-document-converter/compare/v2.3.1...v2.5.0)

Updates `@tailwindcss/vite` from 4.1.18 to 4.2.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.1/packages/@tailwindcss-vite)

Updates `cropperjs` from 1.6.2 to 2.1.0
- [Release notes](https://github.com/fengyuanchen/cropperjs/releases)
- [Changelog](https://github.com/fengyuanchen/cropperjs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fengyuanchen/cropperjs/commits/v2.1.0/packages/cropperjs)

Updates `i18next` from 25.7.3 to 25.8.13
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v25.7.3...v25.8.13)

Updates `i18next-browser-languagedetector` from 8.2.0 to 8.2.1
- [Changelog](https://github.com/i18next/i18next-browser-languageDetector/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next-browser-languageDetector/compare/v8.2.0...v8.2.1)

Updates `lucide` from 0.546.0 to 0.575.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.575.0/packages/lucide)

Updates `mermaid` from 11.12.2 to 11.12.3
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Commits](https://github.com/mermaid-js/mermaid/compare/mermaid@11.12.2...mermaid@11.12.3)

Updates `pdfjs-dist` from 5.4.530 to 5.4.624
- [Release notes](https://github.com/mozilla/pdf.js/releases)
- [Commits](https://github.com/mozilla/pdf.js/compare/v5.4.530...v5.4.624)

Updates `postal-mime` from 2.7.1 to 2.7.3
- [Release notes](https://github.com/postalsys/postal-mime/releases)
- [Changelog](https://github.com/postalsys/postal-mime/blob/master/CHANGELOG.md)
- [Commits](https://github.com/postalsys/postal-mime/compare/v2.7.1...v2.7.3)

Updates `sortablejs` from 1.15.6 to 1.15.7
- [Release notes](https://github.com/SortableJS/Sortable/releases)
- [Commits](https://github.com/SortableJS/Sortable/compare/1.15.6...1.15.7)

Updates `tailwindcss` from 4.1.18 to 4.2.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.1/packages/tailwindcss)

Updates `terser` from 5.44.1 to 5.46.0
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/compare/v5.44.1...v5.46.0)

Updates `tesseract.js` from 6.0.1 to 7.0.0
- [Release notes](https://github.com/naptha/tesseract.js/releases)
- [Commits](https://github.com/naptha/tesseract.js/compare/v6.0.1...v7.0.0)

Updates `vite-plugin-static-copy` from 3.1.4 to 3.2.0
- [Release notes](https://github.com/sapphi-red/vite-plugin-static-copy/releases)
- [Changelog](https://github.com/sapphi-red/vite-plugin-static-copy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sapphi-red/vite-plugin-static-copy/compare/vite-plugin-static-copy@3.1.4...vite-plugin-static-copy@3.2.0)

Updates `@eslint/js` from 9.39.2 to 10.0.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v10.0.1/packages/js)

Updates `@types/pdfkit` from 0.17.4 to 0.17.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/pdfkit)

Updates `@vitest/coverage-v8` from 3.2.4 to 4.0.18
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.18/packages/coverage-v8)

Updates `@vitest/ui` from 3.2.4 to 4.0.18
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.18/packages/ui)

Updates `eslint` from 9.39.2 to 10.0.2
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.2...v10.0.2)

Updates `globals` from 17.0.0 to 17.4.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.0.0...v17.4.0)

Updates `jsdom` from 27.4.0 to 28.1.0
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Changelog](https://github.com/jsdom/jsdom/blob/main/Changelog.md)
- [Commits](https://github.com/jsdom/jsdom/compare/27.4.0...28.1.0)

Updates `lint-staged` from 16.2.7 to 16.3.1
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v16.2.7...v16.3.1)

Updates `prettier` from 3.7.4 to 3.8.1
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.7.4...3.8.1)

Updates `typescript-eslint` from 8.51.0 to 8.56.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.1/packages/typescript-eslint)

Updates `vite` from 7.3.0 to 7.3.1
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.1/packages/vite)

Updates `vite-plugin-node-polyfills` from 0.24.0 to 0.25.0
- [Release notes](https://github.com/davidmyersdev/vite-plugin-node-polyfills/releases)
- [Commits](https://github.com/davidmyersdev/vite-plugin-node-polyfills/compare/v0.24.0...v0.25.0)

Updates `vitest` from 3.2.4 to 4.0.18
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.18/packages/vitest)

Updates `vue` from 3.5.26 to 3.5.29
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/compare/v3.5.26...v3.5.29)

---
updated-dependencies:
- dependency-name: "@kenjiuno/msgreader"
  dependency-version: 1.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@matbee/libreoffice-converter"
  dependency-version: 2.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: cropperjs
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: i18next
  dependency-version: 25.8.13
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: i18next-browser-languagedetector
  dependency-version: 8.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: lucide
  dependency-version: 0.575.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: mermaid
  dependency-version: 11.12.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: pdfjs-dist
  dependency-version: 5.4.624
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: postal-mime
  dependency-version: 2.7.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: sortablejs
  dependency-version: 1.15.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: tailwindcss
  dependency-version: 4.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: terser
  dependency-version: 5.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: tesseract.js
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: vite-plugin-static-copy
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@eslint/js"
  dependency-version: 10.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: "@types/pdfkit"
  dependency-version: 0.17.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 4.0.18
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: "@vitest/ui"
  dependency-version: 4.0.18
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: eslint
  dependency-version: 10.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: globals
  dependency-version: 17.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: jsdom
  dependency-version: 28.1.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: lint-staged
  dependency-version: 16.3.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: prettier
  dependency-version: 3.8.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.56.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: vite
  dependency-version: 7.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: vite-plugin-node-polyfills
  dependency-version: 0.25.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: vitest
  dependency-version: 4.0.18
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: vue
  dependency-version: 3.5.29
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-01 14:26:20 +00:00
dependabot[bot] 9b099c6a6d build(deps): bump svelte from 5.53.1 to 5.53.6
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.53.1 to 5.53.6.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.53.6/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.53.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-01 14:21:39 +00:00
156 changed files with 4680 additions and 2952 deletions
+5 -6
View File
@@ -47,14 +47,13 @@ ENV VITE_BRAND_NAME=$VITE_BRAND_NAME
ENV VITE_BRAND_LOGO=$VITE_BRAND_LOGO ENV VITE_BRAND_LOGO=$VITE_BRAND_LOGO
ENV VITE_FOOTER_TEXT=$VITE_FOOTER_TEXT ENV VITE_FOOTER_TEXT=$VITE_FOOTER_TEXT
ARG VITE_CORS_PROXY_URL
ARG VITE_CORS_PROXY_SECRET
ENV VITE_CORS_PROXY_URL=$VITE_CORS_PROXY_URL
ENV VITE_CORS_PROXY_SECRET=$VITE_CORS_PROXY_SECRET
ENV NODE_OPTIONS="--max-old-space-size=3072" ENV NODE_OPTIONS="--max-old-space-size=3072"
RUN npm run build:with-docs RUN --mount=type=secret,id=VITE_CORS_PROXY_URL \
--mount=type=secret,id=VITE_CORS_PROXY_SECRET \
VITE_CORS_PROXY_URL=$(cat /run/secrets/VITE_CORS_PROXY_URL 2>/dev/null || echo "") \
VITE_CORS_PROXY_SECRET=$(cat /run/secrets/VITE_CORS_PROXY_SECRET 2>/dev/null || echo "") \
npm run build:with-docs
# Production stage # Production stage
FROM quay.io/nginx/nginx-unprivileged:stable-alpine-slim FROM quay.io/nginx/nginx-unprivileged:stable-alpine-slim
+5 -6
View File
@@ -44,14 +44,13 @@ ENV VITE_BRAND_NAME=$VITE_BRAND_NAME
ENV VITE_BRAND_LOGO=$VITE_BRAND_LOGO ENV VITE_BRAND_LOGO=$VITE_BRAND_LOGO
ENV VITE_FOOTER_TEXT=$VITE_FOOTER_TEXT ENV VITE_FOOTER_TEXT=$VITE_FOOTER_TEXT
ARG VITE_CORS_PROXY_URL
ARG VITE_CORS_PROXY_SECRET
ENV VITE_CORS_PROXY_URL=$VITE_CORS_PROXY_URL
ENV VITE_CORS_PROXY_SECRET=$VITE_CORS_PROXY_SECRET
ENV NODE_OPTIONS="--max-old-space-size=3072" ENV NODE_OPTIONS="--max-old-space-size=3072"
RUN npm run build:with-docs RUN --mount=type=secret,id=VITE_CORS_PROXY_URL \
--mount=type=secret,id=VITE_CORS_PROXY_SECRET \
VITE_CORS_PROXY_URL=$(cat /run/secrets/VITE_CORS_PROXY_URL 2>/dev/null || echo "") \
VITE_CORS_PROXY_SECRET=$(cat /run/secrets/VITE_CORS_PROXY_SECRET 2>/dev/null || echo "") \
npm run build:with-docs
# Production stage — uses standard nginx (starts as root, drops to PUID/PGID) # Production stage — uses standard nginx (starts as root, drops to PUID/PGID)
FROM nginx:stable-alpine-slim FROM nginx:stable-alpine-slim
+12 -1
View File
@@ -873,7 +873,10 @@ The **Digital Signature** tool uses a signing library that may need to fetch cer
``` ```
Or with Docker: Or with Docker:
```bash ```bash
docker build --build-arg VITE_CORS_PROXY_URL="https://your-worker-url.workers.dev" -t your-bentopdf . export VITE_CORS_PROXY_URL="https://your-worker-url.workers.dev"
DOCKER_BUILDKIT=1 docker build \
--secret id=VITE_CORS_PROXY_URL,env=VITE_CORS_PROXY_URL \
-t your-bentopdf .
``` ```
#### Production Security Features #### Production Security Features
@@ -927,6 +930,14 @@ npx wrangler secret put PROXY_SECRET
# Set in build environment # Set in build environment
VITE_CORS_PROXY_SECRET=your-secret npm run build VITE_CORS_PROXY_SECRET=your-secret npm run build
# Or with Docker (optional; URL secret also shown for completeness)
export VITE_CORS_PROXY_URL="https://your-worker-url.workers.dev"
export VITE_CORS_PROXY_SECRET="your-secret"
DOCKER_BUILDKIT=1 docker build \
--secret id=VITE_CORS_PROXY_URL,env=VITE_CORS_PROXY_URL \
--secret id=VITE_CORS_PROXY_SECRET,env=VITE_CORS_PROXY_SECRET \
-t your-bentopdf .
``` ```
### 📦 Version Management ### 📦 Version Management
+9
View File
@@ -142,4 +142,13 @@ npx wrangler deploy
VITE_CORS_PROXY_URL=https://your-worker.workers.dev npm run build VITE_CORS_PROXY_URL=https://your-worker.workers.dev npm run build
``` ```
Or with Docker:
```bash
export VITE_CORS_PROXY_URL="https://your-worker.workers.dev"
DOCKER_BUILDKIT=1 docker build \
--secret id=VITE_CORS_PROXY_URL,env=VITE_CORS_PROXY_URL \
-t your-bentopdf .
```
> **Note:** See [README](https://github.com/alam00000/bentopdf#digital-signature-cors-proxy-required) for HMAC signature setup. > **Note:** See [README](https://github.com/alam00000/bentopdf#digital-signature-cors-proxy-required) for HMAC signature setup.
+3 -2
View File
@@ -64,8 +64,9 @@ If you're self-hosting BentoPDF, you'll need to deploy your own CORS proxy for d
If using Docker: If using Docker:
```bash ```bash
docker build \ export VITE_CORS_PROXY_URL="https://your-worker.workers.dev"
--build-arg VITE_CORS_PROXY_URL="https://your-worker.workers.dev" \ DOCKER_BUILDKIT=1 docker build \
--secret id=VITE_CORS_PROXY_URL,env=VITE_CORS_PROXY_URL \
-t your-bentopdf . -t your-bentopdf .
``` ```
+795 -1133
View File
File diff suppressed because it is too large Load Diff
+29 -29
View File
@@ -1,7 +1,7 @@
{ {
"name": "bento-pdf", "name": "bento-pdf",
"private": true, "private": true,
"version": "2.4.0", "version": "2.4.1",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -37,32 +37,32 @@
"prepare": "husky" "prepare": "husky"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.2", "@eslint/js": "^10.0.1",
"@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": "^1.0.0", "@types/html2canvas": "^1.0.0",
"@types/pdfkit": "^0.17.3", "@types/pdfkit": "^0.17.5",
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@types/utif": "^3.0.6", "@types/utif": "^3.0.6",
"@vitejs/plugin-basic-ssl": "^2.1.4", "@vitejs/plugin-basic-ssl": "^2.1.4",
"@vitest/coverage-v8": "^3.2.4", "@vitest/coverage-v8": "^4.0.18",
"@vitest/ui": "^3.2.4", "@vitest/ui": "^4.0.18",
"eslint": "^9.39.2", "eslint": "^10.0.2",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"globals": "^17.0.0", "globals": "^17.4.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"jsdom": "^27.0.1", "jsdom": "^28.1.0",
"lint-staged": "^16.2.7", "lint-staged": "^16.3.1",
"prettier": "^3.6.2", "prettier": "^3.8.1",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"typescript-eslint": "^8.51.0", "typescript-eslint": "^8.56.1",
"vite": "^7.1.11", "vite": "^7.3.1",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-handlebars": "^2.0.0", "vite-plugin-handlebars": "^2.0.0",
"vite-plugin-node-polyfills": "^0.24.0", "vite-plugin-node-polyfills": "^0.25.0",
"vitepress": "^1.6.4", "vitepress": "^1.6.4",
"vitest": "^3.2.4", "vitest": "^4.0.18",
"vue": "^3.5.26" "vue": "^3.5.29"
}, },
"dependencies": { "dependencies": {
"@fontsource/cedarville-cursive": "^5.2.7", "@fontsource/cedarville-cursive": "^5.2.7",
@@ -72,32 +72,32 @@
"@fontsource/kalam": "^5.2.8", "@fontsource/kalam": "^5.2.8",
"@fontsource/lato": "^5.2.7", "@fontsource/lato": "^5.2.7",
"@fontsource/merriweather": "^5.2.11", "@fontsource/merriweather": "^5.2.11",
"@kenjiuno/msgreader": "^1.27.1-alpha.1", "@kenjiuno/msgreader": "^1.28.0",
"@matbee/libreoffice-converter": "^2.3.1", "@matbee/libreoffice-converter": "^2.5.0",
"@neslinesli93/qpdf-wasm": "^0.3.0", "@neslinesli93/qpdf-wasm": "^0.3.0",
"@pdf-lib/fontkit": "^1.1.1", "@pdf-lib/fontkit": "^1.1.1",
"@phosphor-icons/web": "^2.1.2", "@phosphor-icons/web": "^2.1.2",
"@retejs/lit-plugin": "^2.0.7", "@retejs/lit-plugin": "^2.0.7",
"@tailwindcss/vite": "^4.1.15", "@tailwindcss/vite": "^4.2.1",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@types/node-forge": "^1.3.14", "@types/node-forge": "^1.3.14",
"@types/papaparse": "^5.5.2", "@types/papaparse": "^5.5.2",
"archiver": "^7.0.1", "archiver": "^7.0.1",
"blob-stream": "^0.1.3", "blob-stream": "^0.1.3",
"bwip-js": "^4.8.0", "bwip-js": "^4.8.0",
"cropperjs": "^1.6.1", "cropperjs": "^1.6.2",
"embedpdf-snippet": "file:vendor/embedpdf/embedpdf-snippet-2.3.0.tgz", "embedpdf-snippet": "file:vendor/embedpdf/embedpdf-snippet-2.3.0.tgz",
"heic2any": "^0.0.4", "heic2any": "^0.0.4",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"i18next": "^25.7.2", "i18next": "^25.8.13",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.1",
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "^3.0.2",
"jspdf": "^4.2.0", "jspdf": "^4.2.0",
"jspdf-autotable": "^5.0.2", "jspdf-autotable": "^5.0.2",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lit": "^3.3.2", "lit": "^3.3.2",
"lucide": "^0.546.0", "lucide": "^0.575.0",
"markdown-it": "^14.1.1", "markdown-it": "^14.1.1",
"markdown-it-abbr": "^2.0.0", "markdown-it-abbr": "^2.0.0",
"markdown-it-anchor": "^9.2.0", "markdown-it-anchor": "^9.2.0",
@@ -110,25 +110,25 @@
"markdown-it-sup": "^2.0.0", "markdown-it-sup": "^2.0.0",
"markdown-it-task-lists": "^2.1.1", "markdown-it-task-lists": "^2.1.1",
"markdown-it-toc-done-right": "^4.2.0", "markdown-it-toc-done-right": "^4.2.0",
"mermaid": "^11.12.2", "mermaid": "^11.12.3",
"node-forge": "^1.3.3", "node-forge": "^1.3.3",
"papaparse": "^5.5.3", "papaparse": "^5.5.3",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pdfjs-dist": "^5.4.296", "pdfjs-dist": "^5.4.624",
"pdfkit": "^0.17.2", "pdfkit": "^0.17.2",
"postal-mime": "^2.7.1", "postal-mime": "^2.7.3",
"rete": "^2.0.6", "rete": "^2.0.6",
"rete-area-plugin": "^2.1.5", "rete-area-plugin": "^2.1.5",
"rete-connection-plugin": "^2.0.5", "rete-connection-plugin": "^2.0.5",
"rete-engine": "^2.1.1", "rete-engine": "^2.1.1",
"rete-render-utils": "^2.0.3", "rete-render-utils": "^2.0.3",
"sortablejs": "^1.15.6", "sortablejs": "^1.15.7",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
"terser": "^5.44.0", "terser": "^5.46.0",
"tesseract.js": "^6.0.1", "tesseract.js": "^7.0.0",
"tiff": "^7.1.2", "tiff": "^7.1.2",
"utif": "^3.1.0", "utif": "^3.1.0",
"vite-plugin-static-copy": "^3.1.4", "vite-plugin-static-copy": "^3.2.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"zgapdfsigner": "^2.7.5" "zgapdfsigner": "^2.7.5"
}, },
+2 -1
View File
@@ -163,7 +163,8 @@
}, },
"removeBlankPages": { "removeBlankPages": {
"name": "إزالة الصفحات الفارغة", "name": "إزالة الصفحات الفارغة",
"subtitle": "اكتشاف وحذف الصفحات الفارغة تلقائيًا." "subtitle": "اكتشاف وحذف الصفحات الفارغة تلقائيًا.",
"sensitivityHint": "أعلى = أكثر صرامة، فقط الصفحات الفارغة تمامًا. أقل = يسمح بصفحات تحتوي على بعض المحتوى."
}, },
"imageToPdf": { "imageToPdf": {
"name": "صور إلى PDF", "name": "صور إلى PDF",
+2 -1
View File
@@ -163,7 +163,8 @@
}, },
"removeBlankPages": { "removeBlankPages": {
"name": "Выдаліць пустыя старонкі", "name": "Выдаліць пустыя старонкі",
"subtitle": "Аўтаматычна выявіць і выдаліць пустыя старонкі." "subtitle": "Аўтаматычна выявіць і выдаліць пустыя старонкі.",
"sensitivityHint": "Вышэй = больш строга, толькі цалкам пустыя старонкі. Ніжэй = дапускае старонкі з нейкім змесцівам."
}, },
"imageToPdf": { "imageToPdf": {
"name": "Відарысы ў PDF", "name": "Відарысы ў PDF",
+2 -1
View File
@@ -163,7 +163,8 @@
}, },
"removeBlankPages": { "removeBlankPages": {
"name": "Fjern tomme sider", "name": "Fjern tomme sider",
"subtitle": "Find og fjern automatisk tomme sider." "subtitle": "Find og fjern automatisk tomme sider.",
"sensitivityHint": "Højere = strengere, kun helt tomme sider. Lavere = tillader sider med noget indhold."
}, },
"imageToPdf": { "imageToPdf": {
"name": "Billeder til PDF", "name": "Billeder til PDF",
+2 -1
View File
@@ -163,7 +163,8 @@
}, },
"removeBlankPages": { "removeBlankPages": {
"name": "Leere Seiten entfernen", "name": "Leere Seiten entfernen",
"subtitle": "Leere Seiten automatisch erkennen und löschen." "subtitle": "Leere Seiten automatisch erkennen und löschen.",
"sensitivityHint": "Höher = strenger, nur rein leere Seiten. Niedriger = erlaubt Seiten mit etwas Inhalt."
}, },
"imageToPdf": { "imageToPdf": {
"name": "Bilder zu PDF", "name": "Bilder zu PDF",
+2 -1
View File
@@ -163,7 +163,8 @@
}, },
"removeBlankPages": { "removeBlankPages": {
"name": "Remove Blank Pages", "name": "Remove Blank Pages",
"subtitle": "Automatically detect and delete blank pages." "subtitle": "Automatically detect and delete blank pages.",
"sensitivityHint": "Higher = stricter, only purely blank pages. Lower = allows pages with some content."
}, },
"imageToPdf": { "imageToPdf": {
"name": "Images to PDF", "name": "Images to PDF",
+2 -1
View File
@@ -163,7 +163,8 @@
}, },
"removeBlankPages": { "removeBlankPages": {
"name": "Eliminar Páginas en Blanco", "name": "Eliminar Páginas en Blanco",
"subtitle": "Detecta y elimina automáticamente páginas en blanco." "subtitle": "Detecta y elimina automáticamente páginas en blanco.",
"sensitivityHint": "Mayor = más estricto, solo páginas completamente en blanco. Menor = permite páginas con algo de contenido."
}, },
"imageToPdf": { "imageToPdf": {
"name": "Imágenes a PDF", "name": "Imágenes a PDF",
+2 -1
View File
@@ -163,7 +163,8 @@
}, },
"removeBlankPages": { "removeBlankPages": {
"name": "Supprimer les pages blanches", "name": "Supprimer les pages blanches",
"subtitle": "Détecter et supprimer automatiquement les pages vides." "subtitle": "Détecter et supprimer automatiquement les pages vides.",
"sensitivityHint": "Plus élevé = plus strict, uniquement les pages vierges. Plus bas = autorise les pages avec du contenu."
}, },
"imageToPdf": { "imageToPdf": {
"name": "Images vers PDF", "name": "Images vers PDF",
+2 -1
View File
@@ -163,7 +163,8 @@
}, },
"removeBlankPages": { "removeBlankPages": {
"name": "Hapus Halaman Kosong", "name": "Hapus Halaman Kosong",
"subtitle": "Deteksi dan hapus halaman kosong secara otomatis." "subtitle": "Deteksi dan hapus halaman kosong secara otomatis.",
"sensitivityHint": "Lebih tinggi = lebih ketat, hanya halaman yang benar-benar kosong. Lebih rendah = mengizinkan halaman dengan sedikit konten."
}, },
"imageToPdf": { "imageToPdf": {
"name": "Gambar ke PDF", "name": "Gambar ke PDF",
+2 -1
View File
@@ -163,7 +163,8 @@
}, },
"removeBlankPages": { "removeBlankPages": {
"name": "Rimuovi Pagine Vuote", "name": "Rimuovi Pagine Vuote",
"subtitle": "Rileva e elimina automaticamente le pagine vuote." "subtitle": "Rileva e elimina automaticamente le pagine vuote.",
"sensitivityHint": "Più alto = più rigoroso, solo pagine completamente vuote. Più basso = consente pagine con qualche contenuto."
}, },
"imageToPdf": { "imageToPdf": {
"name": "Immagini in PDF", "name": "Immagini in PDF",
+3 -2
View File
@@ -93,7 +93,7 @@
"addWatermark": { "addWatermark": {
"name": "Watermerk toevoegen", "name": "Watermerk toevoegen",
"subtitle": "Tekst of een afbeelding over de pagina's van je PDF stempelen.", "subtitle": "Tekst of een afbeelding over de pagina's van je PDF stempelen.",
"applyToAllPages": "Toepassen op alle pagina's" "applyToAllPages": "Op alle pagina's toepassen"
}, },
"headerFooter": { "headerFooter": {
"name": "Koptekst & Voettekst", "name": "Koptekst & Voettekst",
@@ -163,7 +163,8 @@
}, },
"removeBlankPages": { "removeBlankPages": {
"name": "Blanco pagina's Verwijderen", "name": "Blanco pagina's Verwijderen",
"subtitle": "Automatisch blanco pagina's detecteren en verwijderen." "subtitle": "Automatisch blanco pagina's detecteren en verwijderen.",
"sensitivityHint": "Hoger = strenger, alleen volledig lege pagina's. Lager = staat pagina's met enige inhoud toe."
}, },
"imageToPdf": { "imageToPdf": {
"name": "Afbeelding naar PDF", "name": "Afbeelding naar PDF",
+2 -1
View File
@@ -163,7 +163,8 @@
}, },
"removeBlankPages": { "removeBlankPages": {
"name": "Remover Páginas em Branco", "name": "Remover Páginas em Branco",
"subtitle": "Detecte e exclua automaticamente páginas em branco." "subtitle": "Detecte e exclua automaticamente páginas em branco.",
"sensitivityHint": "Maior = mais rigoroso, apenas páginas totalmente em branco. Menor = permite páginas com algum conteúdo."
}, },
"imageToPdf": { "imageToPdf": {
"name": "Imagem para PDF", "name": "Imagem para PDF",
+2 -1
View File
@@ -163,7 +163,8 @@
}, },
"removeBlankPages": { "removeBlankPages": {
"name": "Ta bort tomma sidor", "name": "Ta bort tomma sidor",
"subtitle": "Automatiskt identifiera och ta bort tomma sidor." "subtitle": "Automatiskt identifiera och ta bort tomma sidor.",
"sensitivityHint": "Högre = striktare, bara helt tomma sidor. Lägre = tillåter sidor med visst innehåll."
}, },
"imageToPdf": { "imageToPdf": {
"name": "Bilder till PDF", "name": "Bilder till PDF",
+2 -1
View File
@@ -163,7 +163,8 @@
}, },
"removeBlankPages": { "removeBlankPages": {
"name": "Boş Sayfaları Kaldır", "name": "Boş Sayfaları Kaldır",
"subtitle": "Boş sayfaları otomatik olarak tespit edin ve silin." "subtitle": "Boş sayfaları otomatik olarak tespit edin ve silin.",
"sensitivityHint": "Yüksek = daha katı, yalnızca tamamen boş sayfalar. Düşük = biraz içerik olan sayfalara izin verir."
}, },
"imageToPdf": { "imageToPdf": {
"name": "Görselden PDF'ye", "name": "Görselden PDF'ye",
+2 -1
View File
@@ -163,7 +163,8 @@
}, },
"removeBlankPages": { "removeBlankPages": {
"name": "Xóa trang trống", "name": "Xóa trang trống",
"subtitle": "Tự động phát hiện và xóa trang trống." "subtitle": "Tự động phát hiện và xóa trang trống.",
"sensitivityHint": "Cao hơn = nghiêm ngặt hơn, chỉ các trang hoàn toàn trống. Thấp hơn = cho phép trang có một số nội dung."
}, },
"imageToPdf": { "imageToPdf": {
"name": "Hình ảnh sang PDF", "name": "Hình ảnh sang PDF",
+2 -1
View File
@@ -163,7 +163,8 @@
}, },
"removeBlankPages": { "removeBlankPages": {
"name": "移除空白頁面", "name": "移除空白頁面",
"subtitle": "自動偵測並刪除空白頁面。" "subtitle": "自動偵測並刪除空白頁面。",
"sensitivityHint": "越高 = 越嚴格,僅偵測純空白頁面。越低 = 允許包含少量內容的頁面。"
}, },
"imageToPdf": { "imageToPdf": {
"name": "圖片轉 PDF", "name": "圖片轉 PDF",
+2 -1
View File
@@ -163,7 +163,8 @@
}, },
"removeBlankPages": { "removeBlankPages": {
"name": "移除空白页", "name": "移除空白页",
"subtitle": "自动检测并删除空白页。" "subtitle": "自动检测并删除空白页。",
"sensitivityHint": "越高 = 越严格,仅检测纯空白页。越低 = 允许包含少量内容的页面。"
}, },
"imageToPdf": { "imageToPdf": {
"name": "图片转 PDF", "name": "图片转 PDF",
+16
View File
@@ -343,6 +343,22 @@
"created_at": "2026-02-27T13:07:34Z", "created_at": "2026-02-27T13:07:34Z",
"repoId": 1074785178, "repoId": 1074785178,
"pullRequestNo": 529 "pullRequestNo": 529
},
{
"name": "Olivetti",
"id": 1502093,
"comment_id": 3984507146,
"created_at": "2026-03-02T13:46:02Z",
"repoId": 1074785178,
"pullRequestNo": 531
},
{
"name": "InstaZDLL",
"id": 72951793,
"comment_id": 3984770306,
"created_at": "2026-03-02T14:33:11Z",
"repoId": 1074785178,
"pullRequestNo": 538
} }
] ]
} }
+14 -1
View File
@@ -1,5 +1,5 @@
// This file centralizes the definition of all available tools, organized by category. // This file centralizes the definition of all available tools, organized by category.
export const categories = [ const baseCategories = [
{ {
name: 'Popular Tools', name: 'Popular Tools',
tools: [ tools: [
@@ -788,3 +788,16 @@ export const categories = [
], ],
}, },
]; ];
const getToolIdFromHref = (href: string): string => {
const match = href.match(/\/([^/]+)\.html$/);
return match?.[1] ?? href;
};
export const categories = baseCategories.map((category) => ({
...category,
tools: category.tools.map((tool) => ({
...tool,
id: getToolIdFromHref(tool.href),
})),
}));
+1 -1
View File
@@ -159,7 +159,7 @@ async function renderThumbnails() {
for (let i = 1; i <= deleteState.totalPages; i++) { for (let i = 1; i <= deleteState.totalPages; i++) {
const page = await deleteState.pdfJsDoc.getPage(i); const page = await deleteState.pdfJsDoc.getPage(i);
const viewport = page.getViewport({ scale: 0.3 }); const viewport = page.getViewport({ scale: 1 });
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = viewport.width; canvas.width = viewport.width;
+172 -184
View File
@@ -1,230 +1,218 @@
import { showAlert } from '../ui.js'; import { showAlert } from '../ui.js';
import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js'; import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js';
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib'; import { fixPageSize as fixPageSizeCore } from '../utils/pdf-operations';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
import { FixPageSizeState } from '@/types'; import { FixPageSizeState } from '@/types';
const pageState: FixPageSizeState = { const pageState: FixPageSizeState = {
file: null, file: null,
}; };
function resetState() { function resetState() {
pageState.file = null; pageState.file = null;
const fileDisplayArea = document.getElementById('file-display-area'); const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = ''; if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options'); const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden'); if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement; const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = ''; if (fileInput) fileInput.value = '';
} }
async function updateUI() { async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area'); const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options'); const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return; if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = ''; fileDisplayArea.innerHTML = '';
if (pageState.file) { if (pageState.file) {
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden'; infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div'); const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name; nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div'); const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400'; metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(pageState.file.size); metaSpan.textContent = formatBytes(pageState.file.size);
infoContainer.append(nameSpan, metaSpan); infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button'); const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>'; removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () { removeBtn.onclick = function () {
resetState(); resetState();
}; };
fileDiv.append(infoContainer, removeBtn); fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv); fileDisplayArea.appendChild(fileDiv);
createIcons({ icons }); createIcons({ icons });
if (toolOptions) toolOptions.classList.remove('hidden'); if (toolOptions) toolOptions.classList.remove('hidden');
} else { } else {
if (toolOptions) toolOptions.classList.add('hidden'); if (toolOptions) toolOptions.classList.add('hidden');
} }
} }
function handleFileSelect(files: FileList | null) { function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) { if (files && files.length > 0) {
const file = files[0]; const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { if (
pageState.file = file; file.type === 'application/pdf' ||
updateUI(); file.name.toLowerCase().endsWith('.pdf')
} ) {
pageState.file = file;
updateUI();
} }
}
} }
async function fixPageSize() { async function fixPageSize() {
if (!pageState.file) { if (!pageState.file) {
showAlert('No File', 'Please upload a PDF file first.'); showAlert('No File', 'Please upload a PDF file first.');
return; return;
} }
const targetSizeKey = (document.getElementById('target-size') as HTMLSelectElement).value; const targetSize = (
const orientation = (document.getElementById('orientation') as HTMLSelectElement).value; document.getElementById('target-size') as HTMLSelectElement
const scalingMode = (document.querySelector('input[name="scaling-mode"]:checked') as HTMLInputElement).value; ).value;
const backgroundColor = hexToRgb((document.getElementById('background-color') as HTMLInputElement).value); const orientation = (
document.getElementById('orientation') as HTMLSelectElement
).value;
const scalingMode = (
document.querySelector(
'input[name="scaling-mode"]:checked'
) as HTMLInputElement
).value;
const backgroundColor = hexToRgb(
(document.getElementById('background-color') as HTMLInputElement).value
);
const loaderModal = document.getElementById('loader-modal'); const loaderModal = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text'); const loaderText = document.getElementById('loader-text');
if (loaderModal) loaderModal.classList.remove('hidden'); if (loaderModal) loaderModal.classList.remove('hidden');
if (loaderText) loaderText.textContent = 'Standardizing pages...'; if (loaderText) loaderText.textContent = 'Standardizing pages...';
try { try {
let targetWidth, targetHeight; const customWidth =
parseFloat(
(document.getElementById('custom-width') as HTMLInputElement)?.value
) || 210;
const customHeight =
parseFloat(
(document.getElementById('custom-height') as HTMLInputElement)?.value
) || 297;
const customUnits =
(document.getElementById('custom-units') as HTMLSelectElement)?.value ||
'mm';
if (targetSizeKey === 'Custom') { const arrayBuffer = await pageState.file.arrayBuffer();
const width = parseFloat((document.getElementById('custom-width') as HTMLInputElement).value); const pdfBytes = new Uint8Array(arrayBuffer);
const height = parseFloat((document.getElementById('custom-height') as HTMLInputElement).value);
const units = (document.getElementById('custom-units') as HTMLSelectElement).value;
if (units === 'in') { const newPdfBytes = await fixPageSizeCore(pdfBytes, {
targetWidth = width * 72; targetSize,
targetHeight = height * 72; orientation,
} else { scalingMode,
// mm backgroundColor,
targetWidth = width * (72 / 25.4); customWidth,
targetHeight = height * (72 / 25.4); customHeight,
} customUnits,
} else { });
[targetWidth, targetHeight] = PageSizes[targetSizeKey as keyof typeof PageSizes];
}
if (orientation === 'landscape' && targetWidth < targetHeight) { downloadFile(
[targetWidth, targetHeight] = [targetHeight, targetWidth]; new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
} else if (orientation === 'portrait' && targetWidth > targetHeight) { 'standardized.pdf'
[targetWidth, targetHeight] = [targetHeight, targetWidth]; );
} showAlert(
'Success',
const arrayBuffer = await pageState.file.arrayBuffer(); 'Page sizes standardized successfully!',
const sourceDoc = await PDFLibDocument.load(arrayBuffer); 'success',
const newDoc = await PDFLibDocument.create(); () => {
resetState();
for (const sourcePage of sourceDoc.getPages()) { }
const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize(); );
const embeddedPage = await newDoc.embedPage(sourcePage); } catch (e) {
console.error(e);
const newPage = newDoc.addPage([targetWidth, targetHeight]); showAlert('Error', 'An error occurred while standardizing pages.');
newPage.drawRectangle({ } finally {
x: 0, if (loaderModal) loaderModal.classList.add('hidden');
y: 0, }
width: targetWidth,
height: targetHeight,
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
});
const scaleX = targetWidth / sourceWidth;
const scaleY = targetHeight / sourceHeight;
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
const scaledWidth = sourceWidth * scale;
const scaledHeight = sourceHeight * scale;
const x = (targetWidth - scaledWidth) / 2;
const y = (targetHeight - scaledHeight) / 2;
newPage.drawPage(embeddedPage, {
x,
y,
width: scaledWidth,
height: scaledHeight,
});
}
const newPdfBytes = await newDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'standardized.pdf'
);
showAlert('Success', 'Page sizes standardized successfully!', 'success', () => { resetState(); });
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while standardizing pages.');
} finally {
if (loaderModal) loaderModal.classList.add('hidden');
}
} }
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement; const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone'); const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn'); const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools'); const backBtn = document.getElementById('back-to-tools');
const targetSizeSelect = document.getElementById('target-size'); const targetSizeSelect = document.getElementById('target-size');
const customSizeWrapper = document.getElementById('custom-size-wrapper'); const customSizeWrapper = document.getElementById('custom-size-wrapper');
if (backBtn) { if (backBtn) {
backBtn.addEventListener('click', function () { backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL; window.location.href = import.meta.env.BASE_URL;
});
}
// Setup custom size toggle
if (targetSizeSelect && customSizeWrapper) {
targetSizeSelect.addEventListener('change', function () {
customSizeWrapper.classList.toggle(
'hidden',
(targetSizeSelect as HTMLSelectElement).value !== 'Custom'
);
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
}); });
} if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
// Setup custom size toggle fileInput.addEventListener('click', function () {
if (targetSizeSelect && customSizeWrapper) { fileInput.value = '';
targetSizeSelect.addEventListener('change', function () { });
customSizeWrapper.classList.toggle( }
'hidden',
(targetSizeSelect as HTMLSelectElement).value !== 'Custom'
);
});
}
if (fileInput && dropZone) { if (processBtn) {
fileInput.addEventListener('change', function (e) { processBtn.addEventListener('click', fixPageSize);
handleFileSelect((e.target as HTMLInputElement).files); }
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', fixPageSize);
}
}); });
+3
View File
@@ -9,6 +9,7 @@ import {
renderPagesProgressively, renderPagesProgressively,
cleanupLazyRendering, cleanupLazyRendering,
} from '../utils/render-utils.js'; } from '../utils/render-utils.js';
import { initPagePreview } from '../utils/page-preview.js';
import { isCpdfAvailable } from '../utils/cpdf-helper.js'; import { isCpdfAvailable } from '../utils/cpdf-helper.js';
import { import {
showWasmRequiredDialog, showWasmRequiredDialog,
@@ -210,6 +211,8 @@ async function renderPageMergeThumbnails() {
}, },
} }
); );
initPagePreview(container, pdfjsDoc);
} }
mergeState.cachedThumbnails = true; mergeState.cachedThumbnails = true;
+330 -270
View File
@@ -1,365 +1,425 @@
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js'; import { showAlert, showLoader, hideLoader } from '../ui.js';
import { readFileAsArrayBuffer, formatBytes, downloadFile, getPDFDocument } from '../utils/helpers.js'; import {
readFileAsArrayBuffer,
formatBytes,
downloadFile,
getPDFDocument,
} from '../utils/helpers.js';
import { initPagePreview } from '../utils/page-preview.js';
import { PDFDocument } from 'pdf-lib'; import { PDFDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
interface OrganizeState { interface OrganizeState {
file: File | null; file: File | null;
pdfDoc: any; pdfDoc: any;
pdfJsDoc: any; pdfJsDoc: any;
totalPages: number; totalPages: number;
sortableInstance: any; sortableInstance: any;
} }
const organizeState: OrganizeState = { const organizeState: OrganizeState = {
file: null, file: null,
pdfDoc: null, pdfDoc: null,
pdfJsDoc: null, pdfJsDoc: null,
totalPages: 0, totalPages: 0,
sortableInstance: null, sortableInstance: null,
}; };
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage); document.addEventListener('DOMContentLoaded', initializePage);
} else { } else {
initializePage(); initializePage();
} }
function initializePage() { function initializePage() {
createIcons({ icons }); createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement; const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone'); const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn'); const processBtn = document.getElementById('process-btn');
if (fileInput) fileInput.addEventListener('change', handleFileUpload); if (fileInput) fileInput.addEventListener('change', handleFileUpload);
if (dropZone) { if (dropZone) {
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('bg-gray-700'); }); dropZone.addEventListener('dragover', (e) => {
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('bg-gray-700'); }); e.preventDefault();
dropZone.addEventListener('drop', (e) => { dropZone.classList.add('bg-gray-700');
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const droppedFiles = e.dataTransfer?.files;
if (droppedFiles && droppedFiles.length > 0) handleFile(droppedFiles[0]);
});
// Clear value on click to allow re-selecting the same file
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
if (processBtn) processBtn.addEventListener('click', saveChanges);
document.getElementById('back-to-tools')?.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
}); });
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const droppedFiles = e.dataTransfer?.files;
if (droppedFiles && droppedFiles.length > 0) handleFile(droppedFiles[0]);
});
// Clear value on click to allow re-selecting the same file
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
const applyOrderBtn = document.getElementById('apply-order-btn'); if (processBtn) processBtn.addEventListener('click', saveChanges);
if (applyOrderBtn) applyOrderBtn.addEventListener('click', applyCustomOrder);
document.getElementById('back-to-tools')?.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
const applyOrderBtn = document.getElementById('apply-order-btn');
if (applyOrderBtn) applyOrderBtn.addEventListener('click', applyCustomOrder);
} }
function applyCustomOrder() { function applyCustomOrder() {
const orderInput = document.getElementById('page-order-input') as HTMLInputElement; const orderInput = document.getElementById(
const grid = document.getElementById('page-grid'); 'page-order-input'
) as HTMLInputElement;
const grid = document.getElementById('page-grid');
if (!orderInput || !grid) return; if (!orderInput || !grid) return;
const orderString = orderInput.value; const orderString = orderInput.value;
if (!orderString) { if (!orderString) {
showAlert('Invalid Order', 'Please enter a page order.'); showAlert('Invalid Order', 'Please enter a page order.');
return; return;
}
const newOrder = orderString.split(',').map((s) => parseInt(s.trim(), 10));
// Validation
const currentGridCount = grid.children.length;
const validNumbers = newOrder.every((n) => !isNaN(n) && n > 0); // Basic check, will validate against available thumbnails
if (!validNumbers) {
showAlert('Invalid Page Numbers', `Please enter positive numbers.`);
return;
}
if (newOrder.length !== currentGridCount) {
showAlert(
'Incorrect Page Count',
`The number of pages specified (${newOrder.length}) does not match the current number of pages in the document (${currentGridCount}). Please provide a complete ordering for all pages.`
);
return;
}
const uniqueNumbers = new Set(newOrder);
if (uniqueNumbers.size !== newOrder.length) {
showAlert(
'Duplicate Page Numbers',
'Please ensure all page numbers in the order are unique.'
);
return;
}
const currentThumbnails = Array.from(grid.children) as HTMLElement[];
const reorderedThumbnails: HTMLElement[] = [];
const foundIndices = new Set();
for (const pageNum of newOrder) {
const originalIndexToFind = pageNum - 1; // pageNum is 1-based, originalPageIndex is 0-based
const foundThumbnail = currentThumbnails.find(
(thumb) =>
thumb.dataset.originalPageIndex === originalIndexToFind.toString()
);
if (foundThumbnail) {
reorderedThumbnails.push(foundThumbnail);
foundIndices.add(originalIndexToFind.toString());
} }
}
const newOrder = orderString.split(',').map(s => parseInt(s.trim(), 10)); const allOriginalIndicesPresent = currentThumbnails.every((thumb) =>
foundIndices.has(thumb.dataset.originalPageIndex)
);
// Validation if (
const currentGridCount = grid.children.length; reorderedThumbnails.length !== currentGridCount ||
const validNumbers = newOrder.every(n => !isNaN(n) && n > 0); // Basic check, will validate against available thumbnails !allOriginalIndicesPresent
if (!validNumbers) { ) {
showAlert('Invalid Page Numbers', `Please enter positive numbers.`); showAlert(
return; 'Invalid Page Order',
} 'The specified page order is incomplete or contains invalid page numbers. Please ensure you provide a new position for every original page.'
);
return;
}
if (newOrder.length !== currentGridCount) { // Clear the grid and append the reordered thumbnails
showAlert('Incorrect Page Count', `The number of pages specified (${newOrder.length}) does not match the current number of pages in the document (${currentGridCount}). Please provide a complete ordering for all pages.`); grid.innerHTML = '';
return; reorderedThumbnails.forEach((thumb) => grid.appendChild(thumb));
}
const uniqueNumbers = new Set(newOrder); initializeSortable(); // Re-initialize sortable on the new order
if (uniqueNumbers.size !== newOrder.length) {
showAlert('Duplicate Page Numbers', 'Please ensure all page numbers in the order are unique.');
return;
}
const currentThumbnails = Array.from(grid.children) as HTMLElement[]; showAlert('Success', 'Pages have been reordered.', 'success');
const reorderedThumbnails: HTMLElement[] = [];
const foundIndices = new Set();
for (const pageNum of newOrder) {
const originalIndexToFind = pageNum - 1; // pageNum is 1-based, originalPageIndex is 0-based
const foundThumbnail = currentThumbnails.find(
thumb => thumb.dataset.originalPageIndex === originalIndexToFind.toString()
);
if (foundThumbnail) {
reorderedThumbnails.push(foundThumbnail);
foundIndices.add(originalIndexToFind.toString());
}
}
const allOriginalIndicesPresent = currentThumbnails.every(thumb => foundIndices.has(thumb.dataset.originalPageIndex));
if (reorderedThumbnails.length !== currentGridCount || !allOriginalIndicesPresent) {
showAlert('Invalid Page Order', 'The specified page order is incomplete or contains invalid page numbers. Please ensure you provide a new position for every original page.');
return;
}
// Clear the grid and append the reordered thumbnails
grid.innerHTML = '';
reorderedThumbnails.forEach(thumb => grid.appendChild(thumb));
initializeSortable(); // Re-initialize sortable on the new order
showAlert('Success', 'Pages have been reordered.', 'success');
} }
function handleFileUpload(e: Event) { function handleFileUpload(e: Event) {
const input = e.target as HTMLInputElement; const input = e.target as HTMLInputElement;
if (input.files && input.files.length > 0) handleFile(input.files[0]); if (input.files && input.files.length > 0) handleFile(input.files[0]);
} }
async function handleFile(file: File) { async function handleFile(file: File) {
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) { if (
showAlert('Invalid File', 'Please select a PDF file.'); file.type !== 'application/pdf' &&
return; !file.name.toLowerCase().endsWith('.pdf')
} ) {
showAlert('Invalid File', 'Please select a PDF file.');
return;
}
showLoader('Loading PDF...'); showLoader('Loading PDF...');
organizeState.file = file; organizeState.file = file;
try { try {
const arrayBuffer = await readFileAsArrayBuffer(file); const arrayBuffer = await readFileAsArrayBuffer(file);
organizeState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true, throwOnInvalidObject: false }); organizeState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, {
organizeState.pdfJsDoc = await getPDFDocument({ data: (arrayBuffer as ArrayBuffer).slice(0) }).promise; ignoreEncryption: true,
organizeState.totalPages = organizeState.pdfDoc.getPageCount(); throwOnInvalidObject: false,
});
organizeState.pdfJsDoc = await getPDFDocument({
data: (arrayBuffer as ArrayBuffer).slice(0),
}).promise;
organizeState.totalPages = organizeState.pdfDoc.getPageCount();
updateFileDisplay(); updateFileDisplay();
await renderThumbnails(); await renderThumbnails();
hideLoader(); hideLoader();
} catch (error) { } catch (error) {
console.error('Error loading PDF:', error); console.error('Error loading PDF:', error);
hideLoader(); hideLoader();
showAlert('Error', 'Failed to load PDF file.'); showAlert('Error', 'Failed to load PDF file.');
} }
} }
function updateFileDisplay() { function updateFileDisplay() {
const fileDisplayArea = document.getElementById('file-display-area'); const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea || !organizeState.file) return; if (!fileDisplayArea || !organizeState.file) return;
fileDisplayArea.innerHTML = ''; fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div'); const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0'; infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div'); const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = organizeState.file.name; nameSpan.textContent = organizeState.file.name;
const metaSpan = document.createElement('div'); const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400'; metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(organizeState.file.size)} • ${organizeState.totalPages} pages`; metaSpan.textContent = `${formatBytes(organizeState.file.size)} • ${organizeState.totalPages} pages`;
infoContainer.append(nameSpan, metaSpan); infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button'); const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>'; removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => resetState(); removeBtn.onclick = () => resetState();
fileDiv.append(infoContainer, removeBtn); fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv); fileDisplayArea.appendChild(fileDiv);
createIcons({ icons }); createIcons({ icons });
} }
function renumberPages() { function renumberPages() {
const grid = document.getElementById('page-grid'); const grid = document.getElementById('page-grid');
if (!grid) return; if (!grid) return;
const labels = grid.querySelectorAll('.page-number'); const labels = grid.querySelectorAll('.page-number');
labels.forEach((label, index) => { labels.forEach((label, index) => {
label.textContent = (index + 1).toString(); label.textContent = (index + 1).toString();
}); });
} }
function attachEventListeners(element: HTMLElement) { function attachEventListeners(element: HTMLElement) {
const duplicateBtn = element.querySelector('.duplicate-btn'); const duplicateBtn = element.querySelector('.duplicate-btn');
const deleteBtn = element.querySelector('.delete-btn'); const deleteBtn = element.querySelector('.delete-btn');
duplicateBtn?.addEventListener('click', (e) => { duplicateBtn?.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
const clone = element.cloneNode(true) as HTMLElement; const clone = element.cloneNode(true) as HTMLElement;
element.after(clone); element.after(clone);
attachEventListeners(clone); attachEventListeners(clone);
renumberPages(); renumberPages();
createIcons({ icons }); createIcons({ icons });
initializeSortable(); initializeSortable();
}); });
deleteBtn?.addEventListener('click', (e) => { deleteBtn?.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
const grid = document.getElementById('page-grid'); const grid = document.getElementById('page-grid');
if (grid && grid.children.length > 1) { if (grid && grid.children.length > 1) {
element.remove(); element.remove();
renumberPages(); renumberPages();
initializeSortable(); initializeSortable();
} 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.'
);
}
});
} }
async function renderThumbnails() { async function renderThumbnails() {
const grid = document.getElementById('page-grid'); const grid = document.getElementById('page-grid');
const processBtn = document.getElementById('process-btn'); const processBtn = document.getElementById('process-btn');
const advancedSettings = document.getElementById('advanced-settings'); const advancedSettings = document.getElementById('advanced-settings');
if (!grid || !processBtn || !advancedSettings) return; if (!grid || !processBtn || !advancedSettings) return;
grid.innerHTML = ''; grid.innerHTML = '';
grid.classList.remove('hidden'); grid.classList.remove('hidden');
processBtn.classList.remove('hidden'); processBtn.classList.remove('hidden');
advancedSettings.classList.remove('hidden'); advancedSettings.classList.remove('hidden');
for (let i = 1; i <= organizeState.totalPages; i++) { for (let i = 1; i <= organizeState.totalPages; i++) {
const page = await organizeState.pdfJsDoc.getPage(i); const page = await organizeState.pdfJsDoc.getPage(i);
const viewport = page.getViewport({ scale: 0.5 }); const viewport = page.getViewport({ scale: 1 });
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = viewport.width; canvas.width = viewport.width;
canvas.height = viewport.height; canvas.height = viewport.height;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
await page.render({ canvasContext: ctx, viewport }).promise; await page.render({ canvasContext: ctx, viewport }).promise;
const 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 =
wrapper.dataset.originalPageIndex = (i - 1).toString(); '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 group';
wrapper.dataset.originalPageIndex = (i - 1).toString();
wrapper.dataset.pageNumber = i.toString();
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 = 'relative';
const img = document.createElement('img'); const img = document.createElement('img');
img.src = canvas.toDataURL(); img.src = canvas.toDataURL();
img.className = 'max-w-full max-h-full object-contain'; img.className = 'rounded-md shadow-md max-w-full h-auto';
imgContainer.appendChild(img); imgContainer.appendChild(img);
const pageLabel = document.createElement('span'); const pageLabel = document.createElement('div');
pageLabel.className = 'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1'; pageLabel.className =
pageLabel.textContent = i.toString(); 'page-number absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg z-10 pointer-events-none';
pageLabel.textContent = i.toString();
imgContainer.appendChild(pageLabel);
const controlsDiv = document.createElement('div'); const controlsDiv = document.createElement('div');
controlsDiv.className = 'flex items-center justify-center gap-4'; controlsDiv.className = 'flex items-center justify-center gap-4';
const duplicateBtn = document.createElement('button'); const duplicateBtn = document.createElement('button');
duplicateBtn.className = 'duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center'; duplicateBtn.className =
duplicateBtn.title = 'Duplicate Page'; 'duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
duplicateBtn.innerHTML = '<i data-lucide="copy-plus" class="w-5 h-5"></i>'; duplicateBtn.title = 'Duplicate Page';
duplicateBtn.innerHTML = '<i data-lucide="copy-plus" class="w-5 h-5"></i>';
const deleteBtn = document.createElement('button'); const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center'; deleteBtn.className =
deleteBtn.title = 'Delete Page'; 'delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
deleteBtn.innerHTML = '<i data-lucide="x-circle" class="w-5 h-5"></i>'; deleteBtn.title = 'Delete Page';
deleteBtn.innerHTML = '<i data-lucide="x-circle" class="w-5 h-5"></i>';
controlsDiv.append(duplicateBtn, deleteBtn); controlsDiv.append(duplicateBtn, deleteBtn);
wrapper.append(imgContainer, pageLabel, controlsDiv); wrapper.append(imgContainer, controlsDiv);
grid.appendChild(wrapper); grid.appendChild(wrapper);
attachEventListeners(wrapper); attachEventListeners(wrapper);
} }
createIcons({ icons }); createIcons({ icons });
initializeSortable(); initializeSortable();
initPagePreview(grid, organizeState.pdfJsDoc);
} }
function initializeSortable() { function initializeSortable() {
const grid = document.getElementById('page-grid'); const grid = document.getElementById('page-grid');
if (!grid) return; if (!grid) return;
if (organizeState.sortableInstance) organizeState.sortableInstance.destroy(); if (organizeState.sortableInstance) organizeState.sortableInstance.destroy();
organizeState.sortableInstance = Sortable.create(grid, { organizeState.sortableInstance = Sortable.create(grid, {
animation: 150, animation: 150,
ghostClass: 'sortable-ghost', ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen', chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag', dragClass: 'sortable-drag',
filter: '.duplicate-btn, .delete-btn', filter: '.duplicate-btn, .delete-btn',
preventOnFilter: true, preventOnFilter: true,
onStart: (evt) => { onStart: (evt) => {
if (evt.item) evt.item.style.opacity = '0.5'; if (evt.item) evt.item.style.opacity = '0.5';
}, },
onEnd: (evt) => { onEnd: (evt) => {
if (evt.item) evt.item.style.opacity = '1'; if (evt.item) evt.item.style.opacity = '1';
}, },
}); });
} }
async function saveChanges() { async function saveChanges() {
showLoader('Building new PDF...'); showLoader('Building new PDF...');
try { try {
const grid = document.getElementById('page-grid'); const grid = document.getElementById('page-grid');
if (!grid) return; if (!grid) return;
const finalPageElements = grid.querySelectorAll('.page-thumbnail'); const finalPageElements = grid.querySelectorAll('.page-thumbnail');
const finalIndices = Array.from(finalPageElements) const finalIndices = Array.from(finalPageElements)
.map(el => parseInt((el as HTMLElement).dataset.originalPageIndex || '', 10)) .map((el) =>
.filter(index => !isNaN(index) && index >= 0); parseInt((el as HTMLElement).dataset.originalPageIndex || '', 10)
)
.filter((index) => !isNaN(index) && index >= 0);
if (finalIndices.length === 0) { if (finalIndices.length === 0) {
showAlert('Error', 'No valid pages to save.'); showAlert('Error', 'No valid pages to save.');
return; return;
}
const newPdf = await PDFDocument.create();
const copiedPages = await newPdf.copyPages(organizeState.pdfDoc, finalIndices);
copiedPages.forEach(page => newPdf.addPage(page));
const pdfBytes = await newPdf.save();
const baseName = organizeState.file?.name.replace('.pdf', '') || 'document';
downloadFile(new Blob([pdfBytes as BlobPart], { type: 'application/pdf' }), `${baseName}_organized.pdf`);
hideLoader();
showAlert('Success', 'PDF organized successfully!', 'success', () => resetState());
} catch (error) {
console.error('Error saving changes:', error);
hideLoader();
showAlert('Error', 'Failed to save changes.');
} }
const newPdf = await PDFDocument.create();
const copiedPages = await newPdf.copyPages(
organizeState.pdfDoc,
finalIndices
);
copiedPages.forEach((page) => newPdf.addPage(page));
const pdfBytes = await newPdf.save();
const baseName = organizeState.file?.name.replace('.pdf', '') || 'document';
downloadFile(
new Blob([pdfBytes as BlobPart], { type: 'application/pdf' }),
`${baseName}_organized.pdf`
);
hideLoader();
showAlert('Success', 'PDF organized successfully!', 'success', () =>
resetState()
);
} catch (error) {
console.error('Error saving changes:', error);
hideLoader();
showAlert('Error', 'Failed to save changes.');
}
} }
function resetState() { function resetState() {
if (organizeState.sortableInstance) { if (organizeState.sortableInstance) {
organizeState.sortableInstance.destroy(); organizeState.sortableInstance.destroy();
organizeState.sortableInstance = null; organizeState.sortableInstance = null;
} }
organizeState.file = null; organizeState.file = null;
organizeState.pdfDoc = null; organizeState.pdfDoc = null;
organizeState.pdfJsDoc = null; organizeState.pdfJsDoc = null;
organizeState.totalPages = 0; organizeState.totalPages = 0;
const grid = document.getElementById('page-grid'); const grid = document.getElementById('page-grid');
if (grid) { if (grid) {
grid.innerHTML = ''; grid.innerHTML = '';
grid.classList.add('hidden'); grid.classList.add('hidden');
} }
document.getElementById('process-btn')?.classList.add('hidden'); document.getElementById('process-btn')?.classList.add('hidden');
document.getElementById('advanced-settings')?.classList.add('hidden'); document.getElementById('advanced-settings')?.classList.add('hidden');
const fileDisplayArea = document.getElementById('file-display-area'); const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = ''; if (fileDisplayArea) fileDisplayArea.innerHTML = '';
} }
File diff suppressed because it is too large Load Diff
+21 -3
View File
@@ -1133,9 +1133,7 @@ function showNodeSettings(node: BaseWorkflowNode) {
{ label: 'Top Right', value: 'top-right' }, { label: 'Top Right', value: 'top-right' },
], ],
orientation: [ orientation: [
{ label: 'Vertical', value: 'vertical' }, { label: 'Auto (Keep Original)', value: 'auto' },
{ label: 'Horizontal', value: 'horizontal' },
{ label: 'Auto', value: 'auto' },
{ label: 'Portrait', value: 'portrait' }, { label: 'Portrait', value: 'portrait' },
{ label: 'Landscape', value: 'landscape' }, { label: 'Landscape', value: 'landscape' },
], ],
@@ -1160,6 +1158,23 @@ function showNodeSettings(node: BaseWorkflowNode) {
{ label: 'Letter', value: 'letter' }, { label: 'Letter', value: 'letter' },
{ label: 'Legal', value: 'legal' }, { label: 'Legal', value: 'legal' },
], ],
targetSize: [
{ label: 'A4', value: 'A4' },
{ label: 'Letter', value: 'Letter' },
{ label: 'Legal', value: 'Legal' },
{ label: 'A3', value: 'A3' },
{ label: 'A5', value: 'A5' },
{ label: 'Tabloid', value: 'Tabloid' },
{ label: 'Custom', value: 'Custom' },
],
scalingMode: [
{ label: 'Fit (keep full page visible)', value: 'fit' },
{ label: 'Fill (cover full target page)', value: 'fill' },
],
customUnits: [
{ label: 'Millimeters (mm)', value: 'mm' },
{ label: 'Inches (in)', value: 'in' },
],
numberFormat: [ numberFormat: [
{ label: 'Simple (1, 2, 3)', value: 'simple' }, { label: 'Simple (1, 2, 3)', value: 'simple' },
{ label: 'Page X of Y', value: 'page_x_of_y' }, { label: 'Page X of Y', value: 'page_x_of_y' },
@@ -1294,6 +1309,9 @@ function showNodeSettings(node: BaseWorkflowNode) {
text: ['text'], text: ['text'],
area: ['x0', 'y0', 'x1', 'y1'], area: ['x0', 'y0', 'x1', 'y1'],
}, },
targetSize: {
Custom: ['customWidth', 'customHeight', 'customUnits'],
},
}; };
const controlWrappers: Record<string, HTMLElement> = {}; const controlWrappers: Record<string, HTMLElement> = {};
+273 -231
View File
@@ -1,65 +1,79 @@
import { PDFDocument } from 'pdf-lib'; import { PDFDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { initPagePreview } from '../utils/page-preview.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
// State // State
const pageState: { const pageState: {
pdfDoc: PDFDocument | null; pdfDoc: PDFDocument | null;
file: File | null; file: File | null;
detectedBlankPages: number[]; detectedBlankPages: number[];
pageThumbnails: Map<number, string>; pageThumbnails: Map<number, string>;
} = { } = {
pdfDoc: null, pdfDoc: null,
file: null, file: null,
detectedBlankPages: [], detectedBlankPages: [],
pageThumbnails: new Map() pageThumbnails: new Map(),
}; };
function showLoader(msg = 'Processing...') { function showLoader(msg = 'Processing...') {
document.getElementById('loader-modal')?.classList.remove('hidden'); document.getElementById('loader-modal')?.classList.remove('hidden');
const txt = document.getElementById('loader-text'); const txt = document.getElementById('loader-text');
if (txt) txt.textContent = msg; if (txt) txt.textContent = msg;
} }
function hideLoader() { document.getElementById('loader-modal')?.classList.add('hidden'); } function hideLoader() {
document.getElementById('loader-modal')?.classList.add('hidden');
}
function showAlert(title: string, msg: string, type = 'error', cb?: () => void) { function showAlert(
const modal = document.getElementById('alert-modal'); title: string,
const t = document.getElementById('alert-title'); msg: string,
const m = document.getElementById('alert-message'); type = 'error',
if (t) t.textContent = title; cb?: () => void
if (m) m.textContent = msg; ) {
modal?.classList.remove('hidden'); const modal = document.getElementById('alert-modal');
const okBtn = document.getElementById('alert-ok'); const t = document.getElementById('alert-title');
if (okBtn) { const m = document.getElementById('alert-message');
const newBtn = okBtn.cloneNode(true) as HTMLElement; if (t) t.textContent = title;
okBtn.replaceWith(newBtn); if (m) m.textContent = msg;
newBtn.addEventListener('click', () => { modal?.classList.remove('hidden');
modal?.classList.add('hidden'); const okBtn = document.getElementById('alert-ok');
if (cb) cb(); if (okBtn) {
}); const newBtn = okBtn.cloneNode(true) as HTMLElement;
} okBtn.replaceWith(newBtn);
newBtn.addEventListener('click', () => {
modal?.classList.add('hidden');
if (cb) cb();
});
}
} }
function downloadFile(blob: Blob, filename: string) { function downloadFile(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.download = filename; a.click(); a.href = url;
URL.revokeObjectURL(url); a.download = filename;
a.click();
URL.revokeObjectURL(url);
} }
function updateFileDisplay() { function updateFileDisplay() {
const area = document.getElementById('file-display-area'); const area = document.getElementById('file-display-area');
if (!area || !pageState.file || !pageState.pdfDoc) return; if (!area || !pageState.file || !pageState.pdfDoc) return;
const fileSize = pageState.file.size < 1024 * 1024 const fileSize =
? `${(pageState.file.size / 1024).toFixed(1)} KB` pageState.file.size < 1024 * 1024
: `${(pageState.file.size / 1024 / 1024).toFixed(2)} MB`; ? `${(pageState.file.size / 1024).toFixed(1)} KB`
const pageCount = pageState.pdfDoc.getPageCount(); : `${(pageState.file.size / 1024 / 1024).toFixed(2)} MB`;
const pageCount = pageState.pdfDoc.getPageCount();
area.innerHTML = ` area.innerHTML = `
<div class="bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors"> <div class="bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
@@ -72,257 +86,285 @@ function updateFileDisplay() {
</div> </div>
</div> </div>
`; `;
createIcons({ icons }); createIcons({ icons });
document.getElementById('remove-file')?.addEventListener('click', resetState); document.getElementById('remove-file')?.addEventListener('click', resetState);
} }
function resetState() { function resetState() {
pageState.pdfDoc = null; pageState.pdfDoc = null;
pageState.file = null; pageState.file = null;
pageState.detectedBlankPages = []; pageState.detectedBlankPages = [];
pageState.pageThumbnails.forEach(url => URL.revokeObjectURL(url)); pageState.pageThumbnails.forEach((url) => URL.revokeObjectURL(url));
pageState.pageThumbnails.clear(); pageState.pageThumbnails.clear();
const area = document.getElementById('file-display-area'); const area = document.getElementById('file-display-area');
if (area) area.innerHTML = ''; if (area) area.innerHTML = '';
document.getElementById('options-panel')?.classList.add('hidden'); document.getElementById('options-panel')?.classList.add('hidden');
document.getElementById('preview-panel')?.classList.add('hidden'); document.getElementById('preview-panel')?.classList.add('hidden');
const inp = document.getElementById('file-input') as HTMLInputElement; const inp = document.getElementById('file-input') as HTMLInputElement;
if (inp) inp.value = ''; if (inp) inp.value = '';
const slider = document.getElementById(
'sensitivity-slider'
) as HTMLInputElement;
if (slider) slider.value = '80';
const sliderLabel = document.getElementById('sensitivity-value');
if (sliderLabel) sliderLabel.textContent = '80';
} }
async function handleFileUpload(file: File) { async function handleFileUpload(file: File) {
if (!file || file.type !== 'application/pdf') { if (!file || file.type !== 'application/pdf') {
showAlert('Error', 'Please upload a valid PDF file.'); showAlert('Error', 'Please upload a valid PDF file.');
return; return;
} }
showLoader('Loading PDF...'); showLoader('Loading PDF...');
try { try {
const buf = await file.arrayBuffer(); const buf = await file.arrayBuffer();
pageState.pdfDoc = await PDFDocument.load(buf); pageState.pdfDoc = await PDFDocument.load(buf);
pageState.file = file; pageState.file = file;
pageState.detectedBlankPages = []; pageState.detectedBlankPages = [];
updateFileDisplay(); updateFileDisplay();
document.getElementById('options-panel')?.classList.remove('hidden'); document.getElementById('options-panel')?.classList.remove('hidden');
document.getElementById('preview-panel')?.classList.add('hidden'); document.getElementById('preview-panel')?.classList.add('hidden');
} catch (e) { } catch (e) {
console.error(e); console.error(e);
showAlert('Error', 'Failed to load PDF file.'); showAlert('Error', 'Failed to load PDF file.');
} finally { } finally {
hideLoader(); hideLoader();
} }
} }
async function isPageBlank(page: any, threshold = 250): Promise<boolean> { async function isPageBlank(
const viewport = page.getViewport({ scale: 0.5 }); // Lower scale for faster processing page: any,
const canvas = document.createElement('canvas'); maxNonWhitePercent = 0.5
const ctx = canvas.getContext('2d'); ): Promise<boolean> {
if (!ctx) return false; const viewport = page.getViewport({ scale: 0.5 });
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return false;
canvas.width = viewport.width; canvas.width = viewport.width;
canvas.height = viewport.height; canvas.height = viewport.height;
await page.render({ canvasContext: ctx, viewport }).promise; await page.render({ canvasContext: ctx, viewport }).promise;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data; const data = imageData.data;
const totalPixels = data.length / 4;
let totalBrightness = 0; let nonWhitePixels = 0;
for (let i = 0; i < data.length; i += 4) { for (let i = 0; i < data.length; i += 4) {
const r = data[i], g = data[i + 1], b = data[i + 2]; const brightness = (data[i] + data[i + 1] + data[i + 2]) / 3;
totalBrightness += (r + g + b) / 3; if (brightness < 240) nonWhitePixels++;
} }
const avgBrightness = totalBrightness / (data.length / 4); const nonWhitePercent = (nonWhitePixels / totalPixels) * 100;
return avgBrightness > threshold; return nonWhitePercent <= maxNonWhitePercent;
} }
async function generateThumbnail(page: any): Promise<string> { async function generateThumbnail(page: any): Promise<string> {
const viewport = page.getViewport({ scale: 0.3 }); const viewport = page.getViewport({ scale: 1 });
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) return ''; if (!ctx) return '';
canvas.width = viewport.width; canvas.width = viewport.width;
canvas.height = viewport.height; canvas.height = viewport.height;
await page.render({ canvasContext: ctx, viewport }).promise; await page.render({ canvasContext: ctx, viewport }).promise;
return canvas.toDataURL('image/jpeg', 0.7); return canvas.toDataURL('image/jpeg', 0.7);
} }
async function detectBlankPages() { async function detectBlankPages() {
if (!pageState.pdfDoc || !pageState.file) return showAlert('Error', 'Please upload a PDF first.'); if (!pageState.pdfDoc || !pageState.file)
return showAlert('Error', 'Please upload a PDF first.');
const sensitivitySlider = document.getElementById('sensitivity-slider') as HTMLInputElement; const sensitivitySlider = document.getElementById(
const sensitivityPercent = parseInt(sensitivitySlider?.value || '80'); 'sensitivity-slider'
const threshold = Math.round(255 - (sensitivityPercent * 2.55)); ) as HTMLInputElement;
const sensitivityPercent = parseInt(sensitivitySlider?.value || '80');
const maxNonWhitePercent = 5 - (sensitivityPercent / 100) * 4.9;
showLoader('Detecting blank pages...'); showLoader('Detecting blank pages...');
try { try {
const pdfData = await pageState.file.arrayBuffer(); const pdfData = await pageState.file.arrayBuffer();
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise; const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
const totalPages = pdfDoc.numPages; const totalPages = pdfDoc.numPages;
pageState.detectedBlankPages = []; pageState.detectedBlankPages = [];
pageState.pageThumbnails.forEach(url => URL.revokeObjectURL(url)); pageState.pageThumbnails.forEach((url) => URL.revokeObjectURL(url));
pageState.pageThumbnails.clear(); pageState.pageThumbnails.clear();
for (let i = 1; i <= totalPages; i++) { for (let i = 1; i <= totalPages; i++) {
const page = await pdfDoc.getPage(i); const page = await pdfDoc.getPage(i);
if (await isPageBlank(page, threshold)) { if (await isPageBlank(page, maxNonWhitePercent)) {
pageState.detectedBlankPages.push(i - 1); // 0-indexed pageState.detectedBlankPages.push(i - 1); // 0-indexed
const thumbnail = await generateThumbnail(page); const thumbnail = await generateThumbnail(page);
pageState.pageThumbnails.set(i - 1, thumbnail); pageState.pageThumbnails.set(i - 1, thumbnail);
} }
}
if (pageState.detectedBlankPages.length === 0) {
showAlert('Info', 'No blank pages detected in this PDF.');
hideLoader();
return;
}
// Show preview panel
updatePreviewPanel();
document.getElementById('preview-panel')?.classList.remove('hidden');
hideLoader();
} catch (e) {
console.error(e);
showAlert('Error', 'Could not detect blank pages.');
hideLoader();
} }
if (pageState.detectedBlankPages.length === 0) {
showAlert('Info', 'No blank pages detected in this PDF.');
hideLoader();
return;
}
// Show preview panel
updatePreviewPanel();
document.getElementById('preview-panel')?.classList.remove('hidden');
const previewContainer = document.getElementById('blank-pages-preview');
if (previewContainer) initPagePreview(previewContainer, pdfDoc);
hideLoader();
} catch (e) {
console.error(e);
showAlert('Error', 'Could not detect blank pages.');
hideLoader();
}
} }
function updatePreviewPanel() { function updatePreviewPanel() {
const previewInfo = document.getElementById('preview-info'); const previewInfo = document.getElementById('preview-info');
const previewContainer = document.getElementById('blank-pages-preview'); const previewContainer = document.getElementById('blank-pages-preview');
if (!previewInfo || !previewContainer) return; if (!previewInfo || !previewContainer) return;
previewInfo.textContent = `Found ${pageState.detectedBlankPages.length} blank page(s). Click on a page to deselect it.`; previewInfo.textContent = `Found ${pageState.detectedBlankPages.length} blank page(s). Click on a page to deselect it.`;
previewContainer.innerHTML = ''; previewContainer.innerHTML = '';
pageState.detectedBlankPages.forEach((pageIndex) => { pageState.detectedBlankPages.forEach((pageIndex) => {
const thumbnail = pageState.pageThumbnails.get(pageIndex) || ''; const thumbnail = pageState.pageThumbnails.get(pageIndex) || '';
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'relative cursor-pointer group'; div.className =
div.dataset.pageIndex = String(pageIndex); 'relative cursor-pointer flex flex-col items-center gap-1 p-2 border-2 border-red-500 rounded-lg bg-gray-700 transition-colors group';
div.dataset.selected = 'true'; div.dataset.pageIndex = String(pageIndex);
div.dataset.selected = 'true';
div.innerHTML = ` div.innerHTML = `
<div class="relative border-2 border-red-500 rounded-lg overflow-hidden transition-all"> <div class="relative">
<img src="${thumbnail}" alt="Page ${pageIndex + 1}" class="w-full h-auto"> <img src="${thumbnail}" alt="Page ${pageIndex + 1}" class="rounded-md shadow-md max-w-full h-auto">
<div class="absolute bottom-0 left-0 right-0 bg-black/70 text-white text-xs text-center py-1"> <div class="absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg z-10 pointer-events-none">
Page ${pageIndex + 1} ${pageIndex + 1}
</div> </div>
<div class="absolute top-1 right-1 bg-red-500 rounded-full w-5 h-5 flex items-center justify-center check-mark"> <div class="absolute top-1 right-1 bg-red-500 rounded-full w-5 h-5 flex items-center justify-center check-mark z-10">
<i data-lucide="check" class="w-3 h-3 text-white"></i> <i data-lucide="check" class="w-3 h-3 text-white"></i>
</div> </div>
</div> </div>
`; `;
div.addEventListener('click', () => togglePageSelection(div, pageIndex)); div.addEventListener('click', () => togglePageSelection(div, pageIndex));
previewContainer.appendChild(div); previewContainer.appendChild(div);
}); });
createIcons({ icons }); createIcons({ icons });
} }
function togglePageSelection(div: HTMLElement, pageIndex: number) { function togglePageSelection(div: HTMLElement, pageIndex: number) {
const isSelected = div.dataset.selected === 'true'; const isSelected = div.dataset.selected === 'true';
const border = div.querySelector('.border-2') as HTMLElement; const checkMark = div.querySelector('.check-mark') as HTMLElement;
const checkMark = div.querySelector('.check-mark') as HTMLElement;
if (isSelected) { if (isSelected) {
div.dataset.selected = 'false'; div.dataset.selected = 'false';
border?.classList.remove('border-red-500'); div.classList.remove('border-red-500');
border?.classList.add('border-gray-500', 'opacity-50'); div.classList.add('border-gray-600', 'opacity-50');
checkMark?.classList.add('hidden'); checkMark?.classList.add('hidden');
} else { } else {
div.dataset.selected = 'true'; div.dataset.selected = 'true';
border?.classList.add('border-red-500'); div.classList.add('border-red-500');
border?.classList.remove('border-gray-500', 'opacity-50'); div.classList.remove('border-gray-600', 'opacity-50');
checkMark?.classList.remove('hidden'); checkMark?.classList.remove('hidden');
} }
} }
async function processRemoveBlankPages() { async function processRemoveBlankPages() {
if (!pageState.pdfDoc || !pageState.file) return showAlert('Error', 'Please upload a PDF first.'); if (!pageState.pdfDoc || !pageState.file)
return showAlert('Error', 'Please upload a PDF first.');
// Get selected pages to remove // Get selected pages to remove
const previewContainer = document.getElementById('blank-pages-preview'); const previewContainer = document.getElementById('blank-pages-preview');
const selectedPages: number[] = []; const selectedPages: number[] = [];
previewContainer?.querySelectorAll('[data-selected="true"]').forEach(el => { previewContainer?.querySelectorAll('[data-selected="true"]').forEach((el) => {
const pageIndex = parseInt((el as HTMLElement).dataset.pageIndex || '-1'); const pageIndex = parseInt((el as HTMLElement).dataset.pageIndex || '-1');
if (pageIndex >= 0) selectedPages.push(pageIndex); if (pageIndex >= 0) selectedPages.push(pageIndex);
}); });
if (selectedPages.length === 0) { if (selectedPages.length === 0) {
showAlert('Info', 'No pages selected for removal.'); showAlert('Info', 'No pages selected for removal.');
return; return;
}
showLoader(`Removing ${selectedPages.length} blank page(s)...`);
try {
const newPdf = await PDFDocument.create();
const pages = pageState.pdfDoc.getPages();
for (let i = 0; i < pages.length; i++) {
if (!selectedPages.includes(i)) {
const [copiedPage] = await newPdf.copyPages(pageState.pdfDoc, [i]);
newPdf.addPage(copiedPage);
}
} }
showLoader(`Removing ${selectedPages.length} blank page(s)...`); const newPdfBytes = await newPdf.save();
try { downloadFile(
const newPdf = await PDFDocument.create(); new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
const pages = pageState.pdfDoc.getPages(); 'blank-pages-removed.pdf'
);
for (let i = 0; i < pages.length; i++) { showAlert(
if (!selectedPages.includes(i)) { 'Success',
const [copiedPage] = await newPdf.copyPages(pageState.pdfDoc, [i]); `Removed ${selectedPages.length} blank page(s) successfully!`,
newPdf.addPage(copiedPage); 'success',
} resetState
} );
} catch (e) {
const newPdfBytes = await newPdf.save(); console.error(e);
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'blank-pages-removed.pdf'); showAlert('Error', 'Could not remove blank pages.');
showAlert('Success', `Removed ${selectedPages.length} blank page(s) successfully!`, 'success', resetState); } finally {
} catch (e) { hideLoader();
console.error(e); }
showAlert('Error', 'Could not remove blank pages.');
} finally {
hideLoader();
}
} }
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement; const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone'); const dropZone = document.getElementById('drop-zone');
const detectBtn = document.getElementById('detect-btn'); const detectBtn = document.getElementById('detect-btn');
const processBtn = document.getElementById('process-btn'); const processBtn = document.getElementById('process-btn');
const sensitivitySlider = document.getElementById('sensitivity-slider') as HTMLInputElement; const sensitivitySlider = document.getElementById(
const sensitivityValue = document.getElementById('sensitivity-value'); 'sensitivity-slider'
) as HTMLInputElement;
const sensitivityValue = document.getElementById('sensitivity-value');
sensitivitySlider?.addEventListener('input', (e) => { sensitivitySlider?.addEventListener('input', (e) => {
const value = (e.target as HTMLInputElement).value; const value = (e.target as HTMLInputElement).value;
if (sensitivityValue) sensitivityValue.textContent = value; if (sensitivityValue) sensitivityValue.textContent = value;
}); });
fileInput?.addEventListener('change', (e) => { fileInput?.addEventListener('change', (e) => {
const f = (e.target as HTMLInputElement).files?.[0]; const f = (e.target as HTMLInputElement).files?.[0];
if (f) handleFileUpload(f); if (f) handleFileUpload(f);
}); });
dropZone?.addEventListener('dragover', (e) => { dropZone?.addEventListener('dragover', (e) => {
e.preventDefault(); e.preventDefault();
dropZone.classList.add('border-indigo-500'); dropZone.classList.add('border-indigo-500');
}); });
dropZone?.addEventListener('dragleave', () => { dropZone?.addEventListener('dragleave', () => {
dropZone.classList.remove('border-indigo-500'); dropZone.classList.remove('border-indigo-500');
}); });
dropZone?.addEventListener('drop', (e) => { dropZone?.addEventListener('drop', (e) => {
e.preventDefault(); e.preventDefault();
dropZone.classList.remove('border-indigo-500'); dropZone.classList.remove('border-indigo-500');
const f = e.dataTransfer?.files[0]; const f = e.dataTransfer?.files[0];
if (f) handleFileUpload(f); if (f) handleFileUpload(f);
}); });
detectBtn?.addEventListener('click', detectBlankPages); detectBtn?.addEventListener('click', detectBlankPages);
processBtn?.addEventListener('click', processRemoveBlankPages); processBtn?.addEventListener('click', processRemoveBlankPages);
document.getElementById('back-to-tools')?.addEventListener('click', () => { document.getElementById('back-to-tools')?.addEventListener('click', () => {
window.location.href = '../../index.html'; window.location.href = '../../index.html';
}); });
}); });
+17 -8
View File
@@ -12,6 +12,7 @@ import {
renderPagesProgressively, renderPagesProgressively,
cleanupLazyRendering, cleanupLazyRendering,
} from '../utils/render-utils.js'; } from '../utils/render-utils.js';
import { initPagePreview } from '../utils/page-preview.js';
import { isCpdfAvailable } from '../utils/cpdf-helper.js'; import { isCpdfAvailable } from '../utils/cpdf-helper.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js'; import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import JSZip from 'jszip'; import JSZip from 'jszip';
@@ -153,18 +154,24 @@ document.addEventListener('DOMContentLoaded', () => {
const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => { const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => {
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.className = wrapper.className =
'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500 relative'; 'page-thumbnail-wrapper p-2 border-2 border-gray-600 rounded-lg cursor-pointer hover:border-indigo-500 bg-gray-700 transition-colors relative group flex flex-col items-center gap-1';
wrapper.dataset.pageIndex = (pageNumber - 1).toString(); wrapper.dataset.pageIndex = (pageNumber - 1).toString();
wrapper.dataset.pageNumber = pageNumber.toString();
const imgContainer = document.createElement('div');
imgContainer.className = 'relative';
const img = document.createElement('img'); const img = document.createElement('img');
img.src = canvas.toDataURL(); img.src = canvas.toDataURL();
img.className = 'rounded-md w-full h-auto'; img.className = 'rounded-md shadow-md max-w-full h-auto';
const p = document.createElement('p'); const pageNumDiv = document.createElement('div');
p.className = 'text-center text-xs mt-1 text-gray-300'; pageNumDiv.className =
p.textContent = `Page ${pageNumber}`; 'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg z-10 pointer-events-none';
pageNumDiv.textContent = pageNumber.toString();
wrapper.append(img, p); imgContainer.append(img, pageNumDiv);
wrapper.appendChild(imgContainer);
const handleSelection = (e: any) => { const handleSelection = (e: any) => {
e.preventDefault(); e.preventDefault();
@@ -174,10 +181,10 @@ document.addEventListener('DOMContentLoaded', () => {
if (isSelected) { if (isSelected) {
wrapper.classList.remove('selected', 'border-indigo-500'); wrapper.classList.remove('selected', 'border-indigo-500');
wrapper.classList.add('border-transparent'); wrapper.classList.add('border-gray-600');
} else { } else {
wrapper.classList.add('selected', 'border-indigo-500'); wrapper.classList.add('selected', 'border-indigo-500');
wrapper.classList.remove('border-transparent'); wrapper.classList.remove('border-gray-600');
} }
}; };
@@ -203,6 +210,8 @@ document.addEventListener('DOMContentLoaded', () => {
createIcons({ icons }); createIcons({ icons });
}, },
}); });
initPagePreview(container, pdf);
} catch (error) { } catch (error) {
console.error('Error rendering visual selector:', error); console.error('Error rendering visual selector:', error);
showAlert('Error', 'Failed to render page previews.'); showAlert('Error', 'Failed to render page previews.');
+1
View File
@@ -50,3 +50,4 @@ export * from './bookmark-pdf-type.ts';
export * from './scanner-effect-type.ts'; export * from './scanner-effect-type.ts';
export * from './adjust-colors-type.ts'; export * from './adjust-colors-type.ts';
export * from './bates-numbering-type.ts'; export * from './bates-numbering-type.ts';
export * from './page-preview-type.ts';
+10
View File
@@ -0,0 +1,10 @@
import { PDFDocumentProxy } from 'pdfjs-dist';
export interface PreviewState {
modal: HTMLElement | null;
pdfjsDoc: PDFDocumentProxy | null;
currentPage: number;
totalPages: number;
isOpen: boolean;
container: HTMLElement | null;
}
+376 -347
View File
@@ -1,177 +1,201 @@
import { resetState } from './state.js'; import { resetState } from './state.js';
import { formatBytes, getPDFDocument } from './utils/helpers.js'; import { formatBytes, getPDFDocument } from './utils/helpers.js';
import { tesseractLanguages } from './config/tesseract-languages.js'; import { tesseractLanguages } from './config/tesseract-languages.js';
import { renderPagesProgressively, cleanupLazyRendering } from './utils/render-utils.js'; import {
renderPagesProgressively,
cleanupLazyRendering,
} from './utils/render-utils.js';
import { initPagePreview } from './utils/page-preview.js';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { getRotationState, updateRotationState } from './utils/rotation-state.js'; import {
getRotationState,
updateRotationState,
} from './utils/rotation-state.js';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { t } from './i18n/i18n'; import { t } from './i18n/i18n';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
// Centralizing DOM element selection // Centralizing DOM element selection
export const dom = { export const dom = {
gridView: document.getElementById('grid-view'), gridView: document.getElementById('grid-view'),
toolGrid: document.getElementById('tool-grid'), toolGrid: document.getElementById('tool-grid'),
toolInterface: document.getElementById('tool-interface'), toolInterface: document.getElementById('tool-interface'),
toolContent: document.getElementById('tool-content'), toolContent: document.getElementById('tool-content'),
backToGridBtn: document.getElementById('back-to-grid'), backToGridBtn: document.getElementById('back-to-grid'),
loaderModal: document.getElementById('loader-modal'), loaderModal: document.getElementById('loader-modal'),
loaderText: document.getElementById('loader-text'), loaderText: document.getElementById('loader-text'),
alertModal: document.getElementById('alert-modal'), alertModal: document.getElementById('alert-modal'),
alertTitle: document.getElementById('alert-title'), alertTitle: document.getElementById('alert-title'),
alertMessage: document.getElementById('alert-message'), alertMessage: document.getElementById('alert-message'),
alertOkBtn: document.getElementById('alert-ok'), alertOkBtn: document.getElementById('alert-ok'),
heroSection: document.getElementById('hero-section'), heroSection: document.getElementById('hero-section'),
featuresSection: document.getElementById('features-section'), featuresSection: document.getElementById('features-section'),
toolsHeader: document.getElementById('tools-header'), toolsHeader: document.getElementById('tools-header'),
dividers: document.querySelectorAll('.section-divider'), dividers: document.querySelectorAll('.section-divider'),
hideSections: document.querySelectorAll('.hide-section'), hideSections: document.querySelectorAll('.hide-section'),
shortcutsModal: document.getElementById('shortcuts-modal'), shortcutsModal: document.getElementById('shortcuts-modal'),
closeShortcutsModalBtn: document.getElementById('close-shortcuts-modal'), closeShortcutsModalBtn: document.getElementById('close-shortcuts-modal'),
shortcutsList: document.getElementById('shortcuts-list'), shortcutsList: document.getElementById('shortcuts-list'),
shortcutSearch: document.getElementById('shortcut-search'), shortcutSearch: document.getElementById('shortcut-search'),
resetShortcutsBtn: document.getElementById('reset-shortcuts-btn'), resetShortcutsBtn: document.getElementById('reset-shortcuts-btn'),
importShortcutsBtn: document.getElementById('import-shortcuts-btn'), importShortcutsBtn: document.getElementById('import-shortcuts-btn'),
exportShortcutsBtn: document.getElementById('export-shortcuts-btn'), exportShortcutsBtn: document.getElementById('export-shortcuts-btn'),
openShortcutsBtn: document.getElementById('open-shortcuts-btn'), openShortcutsBtn: document.getElementById('open-shortcuts-btn'),
warningModal: document.getElementById('warning-modal'), warningModal: document.getElementById('warning-modal'),
warningTitle: document.getElementById('warning-title'), warningTitle: document.getElementById('warning-title'),
warningMessage: document.getElementById('warning-message'), warningMessage: document.getElementById('warning-message'),
warningCancelBtn: document.getElementById('warning-cancel-btn'), warningCancelBtn: document.getElementById('warning-cancel-btn'),
warningConfirmBtn: document.getElementById('warning-confirm-btn'), warningConfirmBtn: document.getElementById('warning-confirm-btn'),
}; };
export const showLoader = (text = t('common.loading'), progress?: number) => { export const showLoader = (text = t('common.loading'), progress?: number) => {
if (dom.loaderText) dom.loaderText.textContent = text; if (dom.loaderText) dom.loaderText.textContent = text;
// Add or update progress bar if progress is provided // Add or update progress bar if progress is provided
const loaderModal = dom.loaderModal; const loaderModal = dom.loaderModal;
if (loaderModal) { if (loaderModal) {
let progressBar = loaderModal.querySelector('.loader-progress-bar') as HTMLElement; let progressBar = loaderModal.querySelector(
let progressContainer = loaderModal.querySelector('.loader-progress-container') as HTMLElement; '.loader-progress-bar'
) as HTMLElement;
let progressContainer = loaderModal.querySelector(
'.loader-progress-container'
) as HTMLElement;
if (progress !== undefined && progress >= 0) { if (progress !== undefined && progress >= 0) {
// Create progress container if it doesn't exist // Create progress container if it doesn't exist
if (!progressContainer) { if (!progressContainer) {
progressContainer = document.createElement('div'); progressContainer = document.createElement('div');
progressContainer.className = 'loader-progress-container w-64 mt-4'; progressContainer.className = 'loader-progress-container w-64 mt-4';
progressContainer.innerHTML = ` progressContainer.innerHTML = `
<div class="bg-gray-700 rounded-full h-2 overflow-hidden"> <div class="bg-gray-700 rounded-full h-2 overflow-hidden">
<div class="loader-progress-bar bg-indigo-500 h-full transition-all duration-300" style="width: 0%"></div> <div class="loader-progress-bar bg-indigo-500 h-full transition-all duration-300" style="width: 0%"></div>
</div> </div>
<p class="loader-progress-text text-xs text-gray-400 mt-1 text-center">0%</p> <p class="loader-progress-text text-xs text-gray-400 mt-1 text-center">0%</p>
`; `;
loaderModal.querySelector('.bg-gray-800')?.appendChild(progressContainer); loaderModal
progressBar = progressContainer.querySelector('.loader-progress-bar') as HTMLElement; .querySelector('.bg-gray-800')
} ?.appendChild(progressContainer);
progressBar = progressContainer.querySelector(
'.loader-progress-bar'
) as HTMLElement;
}
// Update progress // Update progress
if (progressBar) { if (progressBar) {
progressBar.style.width = `${progress}%`; progressBar.style.width = `${progress}%`;
} }
const progressText = progressContainer.querySelector('.loader-progress-text'); const progressText = progressContainer.querySelector(
if (progressText) { '.loader-progress-text'
progressText.textContent = `${Math.round(progress)}%`; );
} if (progressText) {
progressContainer.classList.remove('hidden'); progressText.textContent = `${Math.round(progress)}%`;
} else { }
// Hide progress bar if no progress provided progressContainer.classList.remove('hidden');
if (progressContainer) { } else {
progressContainer.classList.add('hidden'); // Hide progress bar if no progress provided
} if (progressContainer) {
} progressContainer.classList.add('hidden');
}
loaderModal.classList.remove('hidden');
} }
loaderModal.classList.remove('hidden');
}
}; };
export const hideLoader = () => { export const hideLoader = () => {
if (dom.loaderModal) dom.loaderModal.classList.add('hidden'); if (dom.loaderModal) dom.loaderModal.classList.add('hidden');
}; };
export const showAlert = (title: any, message: any, type: string = 'error', callback?: () => void) => { export const showAlert = (
if (dom.alertTitle) dom.alertTitle.textContent = title; title: any,
if (dom.alertMessage) dom.alertMessage.textContent = message; message: any,
if (dom.alertModal) dom.alertModal.classList.remove('hidden'); type: string = 'error',
callback?: () => void
) => {
if (dom.alertTitle) dom.alertTitle.textContent = title;
if (dom.alertMessage) dom.alertMessage.textContent = message;
if (dom.alertModal) dom.alertModal.classList.remove('hidden');
if (dom.alertOkBtn) { if (dom.alertOkBtn) {
const newOkBtn = dom.alertOkBtn.cloneNode(true) as HTMLElement; const newOkBtn = dom.alertOkBtn.cloneNode(true) as HTMLElement;
dom.alertOkBtn.replaceWith(newOkBtn); dom.alertOkBtn.replaceWith(newOkBtn);
dom.alertOkBtn = newOkBtn; dom.alertOkBtn = newOkBtn;
newOkBtn.addEventListener('click', () => { newOkBtn.addEventListener('click', () => {
hideAlert(); hideAlert();
if (callback) callback(); if (callback) callback();
}); });
} }
}; };
export const hideAlert = () => { export const hideAlert = () => {
if (dom.alertModal) dom.alertModal.classList.add('hidden'); if (dom.alertModal) dom.alertModal.classList.add('hidden');
}; };
export const switchView = (view: any) => { export const switchView = (view: any) => {
if (view === 'grid') { if (view === 'grid') {
dom.gridView.classList.remove('hidden'); dom.gridView.classList.remove('hidden');
dom.toolInterface.classList.add('hidden'); dom.toolInterface.classList.add('hidden');
// show hero and features and header // show hero and features and header
dom.heroSection.classList.remove('hidden'); dom.heroSection.classList.remove('hidden');
dom.featuresSection.classList.remove('hidden'); dom.featuresSection.classList.remove('hidden');
dom.toolsHeader.classList.remove('hidden'); dom.toolsHeader.classList.remove('hidden');
// show dividers // show dividers
dom.dividers.forEach((divider) => { dom.dividers.forEach((divider) => {
divider.classList.remove('hidden'); divider.classList.remove('hidden');
}); });
// show hideSections // show hideSections
dom.hideSections.forEach((section) => { dom.hideSections.forEach((section) => {
section.classList.remove('hidden'); section.classList.remove('hidden');
}); });
resetState(); resetState();
} else { } else {
dom.gridView.classList.add('hidden'); dom.gridView.classList.add('hidden');
dom.toolInterface.classList.remove('hidden'); dom.toolInterface.classList.remove('hidden');
dom.featuresSection.classList.add('hidden'); dom.featuresSection.classList.add('hidden');
dom.heroSection.classList.add('hidden'); dom.heroSection.classList.add('hidden');
dom.toolsHeader.classList.add('hidden'); dom.toolsHeader.classList.add('hidden');
dom.dividers.forEach((divider) => { dom.dividers.forEach((divider) => {
divider.classList.add('hidden'); divider.classList.add('hidden');
}); });
dom.hideSections.forEach((section) => { dom.hideSections.forEach((section) => {
section.classList.add('hidden'); section.classList.add('hidden');
}); });
} }
}; };
const thumbnailState = { const thumbnailState = {
sortableInstances: {}, sortableInstances: {},
}; };
function initializeOrganizeSortable(containerId: any) { function initializeOrganizeSortable(containerId: any) {
const container = document.getElementById(containerId); const container = document.getElementById(containerId);
if (!container) return; if (!container) return;
if (thumbnailState.sortableInstances[containerId]) { if (thumbnailState.sortableInstances[containerId]) {
thumbnailState.sortableInstances[containerId].destroy(); thumbnailState.sortableInstances[containerId].destroy();
} }
thumbnailState.sortableInstances[containerId] = Sortable.create(container, { thumbnailState.sortableInstances[containerId] = Sortable.create(container, {
animation: 150, animation: 150,
ghostClass: 'sortable-ghost', ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen', chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag', dragClass: 'sortable-drag',
filter: '.delete-page-btn', filter: '.delete-page-btn',
preventOnFilter: true, preventOnFilter: true,
onStart: function (evt: any) { onStart: function (evt: any) {
evt.item.style.opacity = '0.5'; evt.item.style.opacity = '0.5';
}, },
onEnd: function (evt: any) { onEnd: function (evt: any) {
evt.item.style.opacity = '1'; evt.item.style.opacity = '1';
}, },
}); });
} }
/** /**
@@ -180,243 +204,247 @@ function initializeOrganizeSortable(containerId: any) {
* @param {object} pdfDoc The loaded pdf-lib document instance. * @param {object} pdfDoc The loaded pdf-lib document instance.
*/ */
export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => { export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
const containerId = toolId === 'organize' ? 'page-organizer' : toolId === 'delete-pages' ? 'delete-pages-preview' : 'page-rotator'; const containerId =
const container = document.getElementById(containerId); toolId === 'organize'
if (!container) return; ? 'page-organizer'
: toolId === 'delete-pages'
? 'delete-pages-preview'
: 'page-rotator';
const container = document.getElementById(containerId);
if (!container) return;
container.innerHTML = ''; container.innerHTML = '';
// Cleanup any previous lazy loading observers // Cleanup any previous lazy loading observers
cleanupLazyRendering(); cleanupLazyRendering();
const currentRenderId = Date.now(); const currentRenderId = Date.now();
container.dataset.renderId = currentRenderId.toString(); container.dataset.renderId = currentRenderId.toString();
showLoader(t('multiTool.renderingTitle')); showLoader(t('multiTool.renderingTitle'));
const pdfData = await pdfDoc.save(); const pdfData = await pdfDoc.save();
const pdf = await getPDFDocument({ data: pdfData }).promise; const pdf = await getPDFDocument({ data: pdfData }).promise;
// Function to create wrapper element for each page // Function to create wrapper element for each page
const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => { const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => {
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'. // @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
wrapper.dataset.pageIndex = pageNumber - 1; wrapper.dataset.pageIndex = pageNumber - 1;
const imgContainer = document.createElement('div'); const imgContainer = document.createElement('div');
imgContainer.className = imgContainer.className = 'relative';
'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600';
const img = document.createElement('img'); const img = document.createElement('img');
img.src = canvas.toDataURL(); img.src = canvas.toDataURL();
img.className = 'max-w-full max-h-full object-contain'; img.className = 'rounded-md shadow-md max-w-full h-auto';
imgContainer.appendChild(img); imgContainer.appendChild(img);
if (toolId === 'organize') { const pageNumSpan = document.createElement('div');
wrapper.className = 'page-thumbnail relative group'; pageNumSpan.className =
wrapper.appendChild(imgContainer); 'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg z-10 pointer-events-none';
pageNumSpan.textContent = pageNumber.toString();
const pageNumSpan = document.createElement('span'); if (toolId === 'organize') {
pageNumSpan.className = wrapper.className =
'absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1'; '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 group';
pageNumSpan.textContent = pageNumber.toString();
const deleteBtn = document.createElement('button'); imgContainer.appendChild(pageNumSpan);
deleteBtn.className = wrapper.appendChild(imgContainer);
'delete-page-btn absolute top-1 right-1 bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center';
deleteBtn.innerHTML = '&times;';
deleteBtn.addEventListener('click', (e) => {
(e.currentTarget as HTMLElement).parentElement.remove();
// Renumber remaining pages const deleteBtn = document.createElement('button');
const pages = container.querySelectorAll('.page-thumbnail'); deleteBtn.className =
pages.forEach((page, index) => { 'delete-page-btn absolute top-1 right-1 bg-red-600 hover:bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center z-10';
const numSpan = page.querySelector('span'); deleteBtn.innerHTML = '&times;';
if (numSpan) { deleteBtn.addEventListener('click', (e) => {
numSpan.textContent = (index + 1).toString(); (e.currentTarget as HTMLElement).parentElement.remove();
}
});
initializeOrganizeSortable(containerId); // Renumber remaining pages
}); const pages = container.querySelectorAll('.page-thumbnail');
pages.forEach((page, index) => {
const numSpan = page.querySelector('.bg-indigo-600');
if (numSpan) {
numSpan.textContent = (index + 1).toString();
}
});
wrapper.append(pageNumSpan, deleteBtn); initializeOrganizeSortable(containerId);
} else if (toolId === 'rotate') { });
wrapper.className = 'page-rotator-item flex flex-col items-center gap-2 relative group';
// Read rotation from state (handles "Rotate All" on lazy-loaded pages) wrapper.appendChild(deleteBtn);
const rotationStateArray = getRotationState(); } else if (toolId === 'rotate') {
const pageIndex = pageNumber - 1; wrapper.className =
const initialRotation = rotationStateArray[pageIndex] || 0; 'page-rotator-item flex flex-col items-center gap-2 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors relative group';
wrapper.dataset.rotation = initialRotation.toString(); // Read rotation from state (handles "Rotate All" on lazy-loaded pages)
img.classList.add('transition-transform', 'duration-300'); const rotationStateArray = getRotationState();
const pageIndex = pageNumber - 1;
const initialRotation = rotationStateArray[pageIndex] || 0;
// Apply initial rotation if any wrapper.dataset.rotation = initialRotation.toString();
if (initialRotation !== 0) { img.classList.add('transition-transform', 'duration-300');
img.style.transform = `rotate(${initialRotation}deg)`;
}
wrapper.appendChild(imgContainer); // Apply initial rotation if any
if (initialRotation !== 0) {
img.style.transform = `rotate(${initialRotation}deg)`;
}
// Page Number Overlay (Top Left) imgContainer.appendChild(pageNumSpan);
const pageNumSpan = document.createElement('span'); wrapper.appendChild(imgContainer);
pageNumSpan.className =
'absolute top-2 left-2 bg-gray-900 bg-opacity-75 text-white text-xs font-medium rounded-md px-2 py-1 shadow-sm z-10 pointer-events-none';
pageNumSpan.textContent = pageNumber.toString();
wrapper.appendChild(pageNumSpan);
const controlsDiv = document.createElement('div'); const controlsDiv = document.createElement('div');
controlsDiv.className = 'flex flex-col lg:flex-row items-center justify-center w-full gap-2 px-1'; controlsDiv.className =
'flex flex-col lg:flex-row items-center justify-center w-full gap-2 px-1';
// Custom Stepper Component // Custom Stepper Component
const stepperContainer = document.createElement('div'); const stepperContainer = document.createElement('div');
stepperContainer.className = 'flex items-center border border-gray-600 rounded-md bg-gray-800 overflow-hidden w-24 h-8'; stepperContainer.className =
'flex items-center border border-gray-600 rounded-md bg-gray-800 overflow-hidden w-24 h-8';
const decrementBtn = document.createElement('button'); const decrementBtn = document.createElement('button');
decrementBtn.className = 'px-2 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-r border-gray-600 transition-colors flex items-center justify-center'; decrementBtn.className =
decrementBtn.innerHTML = '<i data-lucide="minus" class="w-3 h-3"></i>'; 'px-2 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-r border-gray-600 transition-colors flex items-center justify-center';
decrementBtn.innerHTML = '<i data-lucide="minus" class="w-3 h-3"></i>';
const angleInput = document.createElement('input'); const angleInput = document.createElement('input');
angleInput.type = 'number'; angleInput.type = 'number';
angleInput.className = 'no-spinner w-full h-full bg-transparent text-white text-xs text-center focus:outline-none appearance-none m-0 p-0 border-none'; angleInput.className =
angleInput.value = initialRotation.toString(); 'no-spinner w-full h-full bg-transparent text-white text-xs text-center focus:outline-none appearance-none m-0 p-0 border-none';
angleInput.placeholder = "0"; angleInput.value = initialRotation.toString();
angleInput.placeholder = '0';
const incrementBtn = document.createElement('button'); const incrementBtn = document.createElement('button');
incrementBtn.className = 'px-2 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-l border-gray-600 transition-colors flex items-center justify-center'; incrementBtn.className =
incrementBtn.innerHTML = '<i data-lucide="plus" class="w-3 h-3"></i>'; 'px-2 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-l border-gray-600 transition-colors flex items-center justify-center';
incrementBtn.innerHTML = '<i data-lucide="plus" class="w-3 h-3"></i>';
// Helper to update rotation // Helper to update rotation
const updateRotation = (newRotation: number) => { const updateRotation = (newRotation: number) => {
const card = wrapper; // Closure capture const card = wrapper; // Closure capture
const imgEl = card.querySelector('img'); const imgEl = card.querySelector('img');
const pageIndex = pageNumber - 1; const pageIndex = pageNumber - 1;
// Update UI // Update UI
angleInput.value = newRotation.toString(); angleInput.value = newRotation.toString();
card.dataset.rotation = newRotation.toString(); card.dataset.rotation = newRotation.toString();
imgEl.style.transform = `rotate(${newRotation}deg)`; imgEl.style.transform = `rotate(${newRotation}deg)`;
// Update State // Update State
updateRotationState(pageIndex, newRotation); updateRotationState(pageIndex, newRotation);
}; };
// Event Listeners // Event Listeners
decrementBtn.addEventListener('click', (e) => { decrementBtn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
const current = parseInt(angleInput.value) || 0; const current = parseInt(angleInput.value) || 0;
updateRotation(current - 1); updateRotation(current - 1);
}); });
incrementBtn.addEventListener('click', (e) => { incrementBtn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
const current = parseInt(angleInput.value) || 0; const current = parseInt(angleInput.value) || 0;
updateRotation(current + 1); updateRotation(current + 1);
}); });
angleInput.addEventListener('change', (e) => { angleInput.addEventListener('change', (e) => {
e.stopPropagation(); e.stopPropagation();
const val = parseInt((e.target as HTMLInputElement).value) || 0; const val = parseInt((e.target as HTMLInputElement).value) || 0;
updateRotation(val); updateRotation(val);
}); });
angleInput.addEventListener('click', (e) => e.stopPropagation()); angleInput.addEventListener('click', (e) => e.stopPropagation());
stepperContainer.append(decrementBtn, angleInput, incrementBtn); stepperContainer.append(decrementBtn, angleInput, incrementBtn);
const rotateBtn = document.createElement('button'); const rotateBtn = document.createElement('button');
rotateBtn.className = 'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-1.5 rounded-md text-gray-200 transition-colors flex-shrink-0'; rotateBtn.className =
rotateBtn.title = 'Rotate +90°'; 'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-1.5 rounded-md text-gray-200 transition-colors flex-shrink-0';
rotateBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-4 h-4"></i>'; rotateBtn.title = 'Rotate +90°';
rotateBtn.addEventListener('click', (e) => { rotateBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-4 h-4"></i>';
e.stopPropagation(); rotateBtn.addEventListener('click', (e) => {
const current = parseInt(angleInput.value) || 0; e.stopPropagation();
updateRotation(current + 90); const current = parseInt(angleInput.value) || 0;
}); updateRotation(current + 90);
});
controlsDiv.append(stepperContainer, rotateBtn); controlsDiv.append(stepperContainer, rotateBtn);
wrapper.appendChild(controlsDiv); wrapper.appendChild(controlsDiv);
} else if (toolId === 'delete-pages') { } else if (toolId === 'delete-pages') {
wrapper.className = 'page-thumbnail relative group cursor-pointer transition-all duration-200'; wrapper.className =
wrapper.dataset.pageNumber = pageNumber.toString(); 'page-thumbnail relative cursor-pointer 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 group';
wrapper.dataset.pageNumber = pageNumber.toString();
const innerContainer = document.createElement('div'); imgContainer.appendChild(pageNumSpan);
innerContainer.className = 'relative w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600 transition-colors duration-200'; wrapper.appendChild(imgContainer);
innerContainer.appendChild(img);
wrapper.appendChild(innerContainer);
const pageNumSpan = document.createElement('span'); wrapper.addEventListener('click', () => {
pageNumSpan.className = const input = document.getElementById(
'absolute top-2 left-2 bg-gray-900 bg-opacity-75 text-white text-xs font-medium rounded-md px-2 py-1 shadow-sm z-10 pointer-events-none'; 'pages-to-delete'
pageNumSpan.textContent = pageNumber.toString(); ) as HTMLInputElement;
wrapper.appendChild(pageNumSpan); if (!input) return;
wrapper.addEventListener('click', () => { const currentVal = input.value;
const input = document.getElementById('pages-to-delete') as HTMLInputElement; let pages = currentVal
if (!input) return; .split(',')
.map((s) => s.trim())
.filter((s) => s);
const pageStr = pageNumber.toString();
const currentVal = input.value; if (pages.includes(pageStr)) {
let pages = currentVal.split(',').map(s => s.trim()).filter(s => s); pages = pages.filter((p) => p !== pageStr);
const pageStr = pageNumber.toString(); } else {
pages.push(pageStr);
if (pages.includes(pageStr)) {
pages = pages.filter(p => p !== pageStr);
} else {
pages.push(pageStr);
}
pages.sort((a, b) => {
const numA = parseInt(a.split('-')[0]);
const numB = parseInt(b.split('-')[0]);
return numA - numB;
});
input.value = pages.join(', ');
input.dispatchEvent(new Event('input'));
});
} }
return wrapper; pages.sort((a, b) => {
}; const numA = parseInt(a.split('-')[0]);
const numB = parseInt(b.split('-')[0]);
return numA - numB;
});
try { input.value = pages.join(', ');
// Render pages progressively with lazy loading
await renderPagesProgressively(
pdf,
container,
createWrapper,
{
batchSize: 8,
useLazyLoading: true,
lazyLoadMargin: '300px',
onProgress: (current, total) => {
showLoader(`Rendering page previews: ${current}/${total}`);
},
onBatchComplete: () => {
createIcons({ icons });
},
shouldCancel: () => {
return container.dataset.renderId !== currentRenderId.toString();
}
}
);
if (toolId === 'organize') { input.dispatchEvent(new Event('input'));
initializeOrganizeSortable(containerId); });
} else if (toolId === 'delete-pages') {
// No sortable needed for delete pages
}
// Reinitialize lucide icons for dynamically added elements
createIcons({ icons });
} catch (error) {
console.error('Error rendering page thumbnails:', error);
showAlert(t('multiTool.error'), t('multiTool.errorRendering'));
} finally {
hideLoader();
} }
return wrapper;
};
try {
// Render pages progressively with lazy loading
await renderPagesProgressively(pdf, container, createWrapper, {
batchSize: 8,
useLazyLoading: true,
lazyLoadMargin: '300px',
onProgress: (current, total) => {
showLoader(`Rendering page previews: ${current}/${total}`);
},
onBatchComplete: () => {
createIcons({ icons });
},
shouldCancel: () => {
return container.dataset.renderId !== currentRenderId.toString();
},
});
if (toolId === 'organize') {
initializeOrganizeSortable(containerId);
} else if (toolId === 'delete-pages') {
// No sortable needed for delete pages
}
// Reinitialize lucide icons for dynamically added elements
createIcons({ icons });
// Attach Quick Look page preview
initPagePreview(container, pdf);
} catch (error) {
console.error('Error rendering page thumbnails:', error);
showAlert(t('multiTool.error'), t('multiTool.errorRendering'));
} finally {
hideLoader();
}
}; };
/** /**
@@ -425,36 +453,36 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
* @param {File[]} files The array of file objects. * @param {File[]} files The array of file objects.
*/ */
export const renderFileDisplay = (container: any, files: any) => { export const renderFileDisplay = (container: any, files: any) => {
container.textContent = ''; container.textContent = '';
if (files.length > 0) { if (files.length > 0) {
files.forEach((file: any) => { files.forEach((file: any) => {
const fileDiv = document.createElement('div'); const fileDiv = document.createElement('div');
fileDiv.className = fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const nameSpan = document.createElement('span'); const nameSpan = document.createElement('span');
nameSpan.className = 'truncate font-medium text-gray-200'; nameSpan.className = 'truncate font-medium text-gray-200';
nameSpan.textContent = file.name; nameSpan.textContent = file.name;
const sizeSpan = document.createElement('span'); const sizeSpan = document.createElement('span');
sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400'; sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400';
sizeSpan.textContent = formatBytes(file.size); sizeSpan.textContent = formatBytes(file.size);
fileDiv.append(nameSpan, sizeSpan); fileDiv.append(nameSpan, sizeSpan);
container.appendChild(fileDiv); container.appendChild(fileDiv);
}); });
} }
}; };
const createFileInputHTML = (options = {}) => { const createFileInputHTML = (options = {}) => {
// @ts-expect-error TS(2339) FIXME: Property 'multiple' does not exist on type '{}'. // @ts-expect-error TS(2339) FIXME: Property 'multiple' does not exist on type '{}'.
const multiple = options.multiple ? 'multiple' : ''; const multiple = options.multiple ? 'multiple' : '';
// @ts-expect-error TS(2339) FIXME: Property 'accept' does not exist on type '{}'. // @ts-expect-error TS(2339) FIXME: Property 'accept' does not exist on type '{}'.
const acceptedFiles = options.accept || 'application/pdf'; const acceptedFiles = options.accept || 'application/pdf';
// @ts-expect-error TS(2339) FIXME: Property 'showControls' does not exist on type '{}... Remove this comment to see the full error message // @ts-expect-error TS(2339) FIXME: Property 'showControls' does not exist on type '{}... Remove this comment to see the full error message
const showControls = options.showControls || false; // NEW: Add this parameter const showControls = options.showControls || false; // NEW: Add this parameter
return ` return `
<div id="drop-zone" class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-900 hover:bg-gray-700 transition-colors duration-300"> <div id="drop-zone" class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-900 hover:bg-gray-700 transition-colors duration-300">
<div class="flex flex-col items-center justify-center pt-5 pb-6"> <div class="flex flex-col items-center justify-center pt-5 pb-6">
<i data-lucide="upload-cloud" class="w-10 h-10 mb-3 text-gray-400"></i> <i data-lucide="upload-cloud" class="w-10 h-10 mb-3 text-gray-400"></i>
@@ -465,7 +493,8 @@ const createFileInputHTML = (options = {}) => {
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" ${multiple} accept="${acceptedFiles}"> <input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" ${multiple} accept="${acceptedFiles}">
</div> </div>
${showControls ${
showControls
? ` ? `
<!-- NEW: Add control buttons for multi-file uploads --> <!-- NEW: Add control buttons for multi-file uploads -->
<div id="file-controls" class="hidden mt-4 flex gap-3"> <div id="file-controls" class="hidden mt-4 flex gap-3">
+215
View File
@@ -0,0 +1,215 @@
import * as pdfjsLib from 'pdfjs-dist';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import { PreviewState } from '@/types';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
const state: PreviewState = {
modal: null,
pdfjsDoc: null,
currentPage: 1,
totalPages: 0,
isOpen: false,
container: null,
};
function getOrCreateModal(): HTMLElement {
if (state.modal) return state.modal;
const modal = document.createElement('div');
modal.id = 'page-preview-modal';
modal.className =
'fixed inset-0 bg-black/80 backdrop-blur-sm z-[60] flex items-center justify-center opacity-0 pointer-events-none transition-opacity duration-200';
modal.innerHTML = `
<button id="preview-close" class="absolute top-4 right-4 text-white/70 hover:text-white z-10 transition-colors" title="Close (Esc)">
<svg class="w-8 h-8" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
<button id="preview-prev" class="absolute left-4 top-1/2 -translate-y-1/2 text-white/50 hover:text-white transition-colors p-2" title="Previous page">
<svg class="w-10 h-10" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"/></svg>
</button>
<button id="preview-next" class="absolute right-4 top-1/2 -translate-y-1/2 text-white/50 hover:text-white transition-colors p-2" title="Next page">
<svg class="w-10 h-10" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/></svg>
</button>
<div id="preview-canvas-container" class="flex items-center justify-center max-w-[90vw] max-h-[85vh]">
<div id="preview-loading" class="text-white/60 text-sm">Loading...</div>
</div>
<div id="preview-page-info" class="absolute bottom-6 left-1/2 -translate-x-1/2 bg-gray-900/80 text-white text-sm px-4 py-2 rounded-full backdrop-blur-sm"></div>
`;
modal.addEventListener('click', (e) => {
if (e.target === modal) hidePreview();
});
modal.querySelector('#preview-close')!.addEventListener('click', hidePreview);
modal
.querySelector('#preview-prev')!
.addEventListener('click', () => navigatePage(-1));
modal
.querySelector('#preview-next')!
.addEventListener('click', () => navigatePage(1));
document.body.appendChild(modal);
state.modal = modal;
return modal;
}
async function renderPreviewPage(pageNumber: number): Promise<void> {
if (!state.pdfjsDoc) return;
const modal = getOrCreateModal();
const container = modal.querySelector(
'#preview-canvas-container'
) as HTMLElement;
const pageInfo = modal.querySelector('#preview-page-info') as HTMLElement;
const prevBtn = modal.querySelector('#preview-prev') as HTMLElement;
const nextBtn = modal.querySelector('#preview-next') as HTMLElement;
container.innerHTML = '<div class="text-white/60 text-sm">Loading...</div>';
pageInfo.textContent = `Page ${pageNumber} of ${state.totalPages}`;
prevBtn.style.visibility = pageNumber > 1 ? 'visible' : 'hidden';
nextBtn.style.visibility =
pageNumber < state.totalPages ? 'visible' : 'hidden';
try {
const page = await state.pdfjsDoc.getPage(pageNumber);
const scale = 2.0;
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
canvas.className =
'max-w-[90vw] max-h-[85vh] object-contain rounded-lg shadow-2xl';
canvas.style.width = 'auto';
canvas.style.height = 'auto';
canvas.style.maxWidth = '90vw';
canvas.style.maxHeight = '85vh';
const ctx = canvas.getContext('2d')!;
await page.render({ canvasContext: ctx, viewport, canvas }).promise;
container.innerHTML = '';
container.appendChild(canvas);
state.currentPage = pageNumber;
} catch (err) {
console.error('Preview render error:', err);
container.innerHTML =
'<div class="text-red-400 text-sm">Failed to render page</div>';
}
}
function navigatePage(delta: number): void {
const newPage = state.currentPage + delta;
if (newPage >= 1 && newPage <= state.totalPages) {
renderPreviewPage(newPage);
}
}
export function showPreview(
pdfjsDoc: PDFDocumentProxy,
pageNumber: number,
totalPages: number
): void {
state.pdfjsDoc = pdfjsDoc;
state.totalPages = totalPages;
state.isOpen = true;
const modal = getOrCreateModal();
modal.classList.remove('opacity-0', 'pointer-events-none');
document.body.style.overflow = 'hidden';
renderPreviewPage(pageNumber);
}
export function hidePreview(): void {
if (!state.modal) return;
state.isOpen = false;
state.modal.classList.add('opacity-0', 'pointer-events-none');
document.body.style.overflow = '';
}
function handleKeydown(e: KeyboardEvent): void {
if (!state.isOpen) return;
switch (e.key) {
case 'Escape':
hidePreview();
break;
case 'ArrowLeft':
e.preventDefault();
navigatePage(-1);
break;
case 'ArrowRight':
e.preventDefault();
navigatePage(1);
break;
}
}
document.addEventListener('keydown', handleKeydown);
export function initPagePreview(
container: HTMLElement,
pdfjsDoc: PDFDocumentProxy,
options: { pageAttr?: string } = {}
): void {
const totalPages = pdfjsDoc.numPages;
const thumbnails = container.querySelectorAll<HTMLElement>(
'[data-page-number], [data-page-index], [data-pageIndex]'
);
thumbnails.forEach((thumb) => {
if (thumb.dataset.previewInit) return;
thumb.dataset.previewInit = 'true';
let pageNum = 1;
if (thumb.dataset.pageNumber) {
pageNum = parseInt(thumb.dataset.pageNumber, 10);
} else if (thumb.dataset.pageIndex !== undefined) {
pageNum = parseInt(thumb.dataset.pageIndex, 10) + 1;
}
const icon = document.createElement('button');
icon.className =
'page-preview-btn absolute bottom-1 right-1 bg-gray-900/80 hover:bg-indigo-600 text-white/70 hover:text-white rounded-full w-7 h-7 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity z-10';
icon.title = 'Preview';
icon.innerHTML =
'<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>';
icon.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
showPreview(pdfjsDoc, pageNum, totalPages);
});
if (!thumb.classList.contains('relative')) {
thumb.classList.add('relative');
}
if (!thumb.classList.contains('group')) {
thumb.classList.add('group');
}
thumb.appendChild(icon);
});
container.addEventListener('keydown', (e) => {
if (e.key === ' ' && !state.isOpen) {
const hovered = container.querySelector<HTMLElement>(
'[data-preview-init]:hover'
);
if (hovered) {
e.preventDefault();
let pageNum = 1;
if (hovered.dataset.pageNumber) {
pageNum = parseInt(hovered.dataset.pageNumber, 10);
} else if (hovered.dataset.pageIndex !== undefined) {
pageNum = parseInt(hovered.dataset.pageIndex, 10) + 1;
}
showPreview(pdfjsDoc, pageNum, totalPages);
}
}
});
}
+86 -2
View File
@@ -1,4 +1,4 @@
import { PDFDocument, degrees, rgb, StandardFonts } from 'pdf-lib'; import { PDFDocument, degrees, rgb, StandardFonts, PageSizes } from 'pdf-lib';
export async function mergePdfs( export async function mergePdfs(
pdfBytesList: Uint8Array[] pdfBytesList: Uint8Array[]
@@ -12,7 +12,7 @@ export async function mergePdfs(
); );
copiedPages.forEach((page) => mergedDoc.addPage(page)); copiedPages.forEach((page) => mergedDoc.addPage(page));
} }
return new Uint8Array(await mergedDoc.save()); return new Uint8Array(await mergedDoc.save({ addDefaultPage: false }));
} }
export async function splitPdf( export async function splitPdf(
@@ -438,3 +438,87 @@ export async function addPageNumbers(
return new Uint8Array(await pdfDoc.save()); return new Uint8Array(await pdfDoc.save());
} }
export interface FixPageSizeOptions {
targetSize: string;
orientation: string;
scalingMode: string;
backgroundColor: { r: number; g: number; b: number };
customWidth?: number;
customHeight?: number;
customUnits?: string;
}
export async function fixPageSize(
pdfBytes: Uint8Array,
options: FixPageSizeOptions
): Promise<Uint8Array> {
let targetWidth: number;
let targetHeight: number;
if (options.targetSize.toLowerCase() === 'custom') {
const w = options.customWidth ?? 210;
const h = options.customHeight ?? 297;
const units = (options.customUnits ?? 'mm').toLowerCase();
if (units === 'in') {
targetWidth = w * 72;
targetHeight = h * 72;
} else {
targetWidth = w * (72 / 25.4);
targetHeight = h * (72 / 25.4);
}
} else {
const selected =
PageSizes[options.targetSize as keyof typeof PageSizes] || PageSizes.A4;
targetWidth = selected[0];
targetHeight = selected[1];
}
const orientation = options.orientation.toLowerCase();
if (orientation === 'landscape' && targetWidth < targetHeight) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
} else if (orientation === 'portrait' && targetWidth > targetHeight) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
}
const sourceDoc = await PDFDocument.load(pdfBytes);
const outputDoc = await PDFDocument.create();
for (const sourcePage of sourceDoc.getPages()) {
const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize();
const embeddedPage = await outputDoc.embedPage(sourcePage);
const outputPage = outputDoc.addPage([targetWidth, targetHeight]);
outputPage.drawRectangle({
x: 0,
y: 0,
width: targetWidth,
height: targetHeight,
color: rgb(
options.backgroundColor.r,
options.backgroundColor.g,
options.backgroundColor.b
),
});
const scaleX = targetWidth / sourceWidth;
const scaleY = targetHeight / sourceHeight;
const useFill = options.scalingMode.toLowerCase() === 'fill';
const scale = useFill ? Math.max(scaleX, scaleY) : Math.min(scaleX, scaleY);
const scaledWidth = sourceWidth * scale;
const scaledHeight = sourceHeight * scale;
const x = (targetWidth - scaledWidth) / 2;
const y = (targetHeight - scaledHeight) / 2;
outputPage.drawPage(embeddedPage, {
x,
y,
width: scaledWidth,
height: scaledHeight,
});
}
return new Uint8Array(await outputDoc.save());
}
+5 -5
View File
@@ -24,7 +24,7 @@ export interface RenderConfig {
*/ */
interface PageTask { interface PageTask {
pageNumber: number; pageNumber: number;
pdfjsDoc: any; pdfjsDoc: pdfjsLib.PDFDocumentProxy;
fileName?: string; fileName?: string;
container: HTMLElement; container: HTMLElement;
scale?: number; scale?: number;
@@ -92,9 +92,9 @@ export function createPlaceholder(
* Renders a single page to canvas * Renders a single page to canvas
*/ */
export async function renderPageToCanvas( export async function renderPageToCanvas(
pdfjsDoc: any, pdfjsDoc: pdfjsLib.PDFDocumentProxy,
pageNumber: number, pageNumber: number,
scale: number = 0.5 scale: number = 1
): Promise<HTMLCanvasElement> { ): Promise<HTMLCanvasElement> {
const page = await pdfjsDoc.getPage(pageNumber); const page = await pdfjsDoc.getPage(pageNumber);
const viewport = page.getViewport({ scale }); const viewport = page.getViewport({ scale });
@@ -260,7 +260,7 @@ function requestIdleCallbackPolyfill(callback: () => void): void {
* Main function to render pages progressively with optional lazy loading * Main function to render pages progressively with optional lazy loading
*/ */
export async function renderPagesProgressively( export async function renderPagesProgressively(
pdfjsDoc: any, pdfjsDoc: pdfjsLib.PDFDocumentProxy,
container: HTMLElement, container: HTMLElement,
createWrapper: ( createWrapper: (
canvas: HTMLCanvasElement, canvas: HTMLCanvasElement,
@@ -298,7 +298,7 @@ export async function renderPagesProgressively(
pageNumber: i, pageNumber: i,
pdfjsDoc, pdfjsDoc,
container, container,
scale: useLazyLoading ? 0.3 : 0.5, scale: useLazyLoading ? 0.5 : 1,
createWrapper, createWrapper,
placeholderElement: placeholders[i - 1], placeholderElement: placeholders[i - 1],
}); });
+104
View File
@@ -0,0 +1,104 @@
import { ClassicPreset } from 'rete';
import { BaseWorkflowNode } from './base-node';
import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { fixPageSize } from '../../utils/pdf-operations';
import { PDFDocument } from 'pdf-lib';
import { hexToRgb } from '../../utils/helpers.js';
export class FixPageSizeNode extends BaseWorkflowNode {
readonly category = 'Organize & Manage' as const;
readonly icon = 'ph-frame-corners';
readonly description = 'Standardize all pages to a target size';
constructor() {
super('Fix Page Size');
this.addInput('pdf', new ClassicPreset.Input(pdfSocket, 'PDF'));
this.addOutput(
'pdf',
new ClassicPreset.Output(pdfSocket, 'Standardized PDF')
);
this.addControl(
'targetSize',
new ClassicPreset.InputControl('text', { initial: 'A4' })
);
this.addControl(
'orientation',
new ClassicPreset.InputControl('text', { initial: 'auto' })
);
this.addControl(
'scalingMode',
new ClassicPreset.InputControl('text', { initial: 'fit' })
);
this.addControl(
'backgroundColor',
new ClassicPreset.InputControl('text', { initial: '#ffffff' })
);
this.addControl(
'customWidth',
new ClassicPreset.InputControl('number', { initial: 210 })
);
this.addControl(
'customHeight',
new ClassicPreset.InputControl('number', { initial: 297 })
);
this.addControl(
'customUnits',
new ClassicPreset.InputControl('text', { initial: 'mm' })
);
}
async data(
inputs: Record<string, SocketData[]>
): Promise<Record<string, SocketData>> {
const pdfInputs = requirePdfInput(inputs, 'Fix Page Size');
const getText = (key: string, fallback: string) => {
const ctrl = this.controls[key] as
| ClassicPreset.InputControl<'text'>
| undefined;
return (ctrl?.value || fallback).trim();
};
const getNum = (key: string, fallback: number) => {
const ctrl = this.controls[key] as
| ClassicPreset.InputControl<'number'>
| undefined;
const value = ctrl?.value;
return Number.isFinite(value) ? (value as number) : fallback;
};
const targetSize = getText('targetSize', 'A4');
const orientation = getText('orientation', 'auto');
const scalingMode = getText('scalingMode', 'fit');
const backgroundHex = getText('backgroundColor', '#ffffff');
const customWidth = Math.max(1, getNum('customWidth', 210));
const customHeight = Math.max(1, getNum('customHeight', 297));
const customUnits = getText('customUnits', 'mm');
const backgroundColor = hexToRgb(backgroundHex);
return {
pdf: await processBatch(pdfInputs, async (input) => {
const resultBytes = await fixPageSize(input.bytes, {
targetSize,
orientation,
scalingMode,
backgroundColor,
customWidth,
customHeight,
customUnits,
});
const resultDoc = await PDFDocument.load(resultBytes);
return {
type: 'pdf',
document: resultDoc,
bytes: resultBytes,
filename: input.filename.replace(/\.pdf$/i, '_standardized.pdf'),
};
}),
};
}
}
+8
View File
@@ -13,6 +13,7 @@ import { ReversePagesNode } from './reverse-pages-node';
import { AddBlankPageNode } from './add-blank-page-node'; import { AddBlankPageNode } from './add-blank-page-node';
import { DividePagesNode } from './divide-pages-node'; import { DividePagesNode } from './divide-pages-node';
import { NUpNode } from './n-up-node'; import { NUpNode } from './n-up-node';
import { FixPageSizeNode } from './fix-page-size-node';
import { CombineSinglePageNode } from './combine-single-page-node'; import { CombineSinglePageNode } from './combine-single-page-node';
import { CropNode } from './crop-node'; import { CropNode } from './crop-node';
import { GreyscaleNode } from './greyscale-node'; import { GreyscaleNode } from './greyscale-node';
@@ -298,6 +299,13 @@ export const nodeRegistry: Record<string, NodeRegistryEntry> = {
description: 'Arrange multiple pages per sheet', description: 'Arrange multiple pages per sheet',
factory: () => new NUpNode(), factory: () => new NUpNode(),
}, },
FixPageSizeNode: {
label: 'Fix Page Size',
category: 'Organize & Manage',
icon: 'ph-frame-corners',
description: 'Standardize all pages to a target size',
factory: () => new FixPageSizeNode(),
},
CombineSinglePageNode: { CombineSinglePageNode: {
label: 'Combine to Single Page', label: 'Combine to Single Page',
category: 'Organize & Manage', category: 'Organize & Manage',
@@ -23,7 +23,7 @@ export class RemoveBlankPagesNode extends BaseWorkflowNode {
private async isPageBlank( private async isPageBlank(
page: pdfjsLib.PDFPageProxy, page: pdfjsLib.PDFPageProxy,
threshold: number maxNonWhitePercent: number
): Promise<boolean> { ): Promise<boolean> {
const viewport = page.getViewport({ scale: 0.5 }); const viewport = page.getViewport({ scale: 0.5 });
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
@@ -34,12 +34,14 @@ export class RemoveBlankPagesNode extends BaseWorkflowNode {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data; const data = imageData.data;
let totalBrightness = 0; const totalPixels = data.length / 4;
let nonWhitePixels = 0;
for (let i = 0; i < data.length; i += 4) { for (let i = 0; i < data.length; i += 4) {
totalBrightness += (data[i] + data[i + 1] + data[i + 2]) / 3; const brightness = (data[i] + data[i + 1] + data[i + 2]) / 3;
if (brightness < 240) nonWhitePixels++;
} }
const avgBrightness = totalBrightness / (data.length / 4); const nonWhitePercent = (nonWhitePixels / totalPixels) * 100;
return avgBrightness > threshold; return nonWhitePercent <= maxNonWhitePercent;
} }
async data( async data(
@@ -50,7 +52,10 @@ export class RemoveBlankPagesNode extends BaseWorkflowNode {
const threshCtrl = this.controls['threshold'] as const threshCtrl = this.controls['threshold'] as
| ClassicPreset.InputControl<'number'> | ClassicPreset.InputControl<'number'>
| undefined; | undefined;
const threshold = Math.max(200, Math.min(255, threshCtrl?.value ?? 250)); const maxNonWhitePercent = Math.max(
0.1,
Math.min(5, threshCtrl?.value ?? 0.5)
);
return { return {
pdf: await processBatch(pdfInputs, async (input) => { pdf: await processBatch(pdfInputs, async (input) => {
@@ -61,7 +66,7 @@ export class RemoveBlankPagesNode extends BaseWorkflowNode {
for (let i = 1; i <= pdfjsDoc.numPages; i++) { for (let i = 1; i <= pdfjsDoc.numPages; i++) {
const page = await pdfjsDoc.getPage(i); const page = await pdfjsDoc.getPage(i);
const blank = await this.isPageBlank(page, threshold); const blank = await this.isPageBlank(page, maxNonWhitePercent);
if (!blank) { if (!blank) {
nonBlankIndices.push(i - 1); nonBlankIndices.push(i - 1);
} else { } else {
+2 -2
View File
@@ -132,10 +132,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:addAttachments.name" data-i18n="tools:addAttachments.name"
> >
Add Attachments Free Online - Fast & Secure Add Attachments
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:addAttachments.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:addAttachments.subtitle">
Upload a PDF, then add files to embed as attachments. Embed one or more files into your PDF.
</p> </p>
<!-- PDF Upload --> <!-- PDF Upload -->
+2 -2
View File
@@ -126,10 +126,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:addBlankPage.name" data-i18n="tools:addBlankPage.name"
> >
Add Blank Page Free Online - Fast & Secure Add Blank Page
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:addBlankPage.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:addBlankPage.subtitle">
Insert one or more blank pages at any position in your PDF document. Insert an empty page anywhere in your PDF.
</p> </p>
<div <div
+2 -3
View File
@@ -116,11 +116,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:addStamps.name" data-i18n="tools:addStamps.name"
> >
Add Stamps Free Online - Fast & Secure Add Stamps
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:addStamps.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:addStamps.subtitle">
Upload a PDF, add image stamps using the toolbar, and then save the Add image stamps to your PDF using the annotation toolbar.
stamped PDF.
</p> </p>
<div class="mb-4"> <div class="mb-4">
+2 -3
View File
@@ -120,11 +120,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:addWatermark.name" data-i18n="tools:addWatermark.name"
> >
Add Watermark to PDF Free - Protect PDFs Add Watermark
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:addWatermark.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:addWatermark.subtitle">
Add text or image watermarks to your PDF documents with customizable Stamp text or an image over your PDF pages.
opacity and rotation.
</p> </p>
<div <div
+2 -2
View File
@@ -118,10 +118,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:adjustColors.name" data-i18n="tools:adjustColors.name"
> >
Adjust Colors Free Online - PDF Color Correction Tool Adjust Colors
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:adjustColors.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:adjustColors.subtitle">
Fine-tune brightness, contrast, saturation, hue and more in your PDF. Fine-tune brightness, contrast, saturation and more in your PDF.
</p> </p>
<div <div
+2 -3
View File
@@ -132,11 +132,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:alternateMix.name" data-i18n="tools:alternateMix.name"
> >
Alternate Merge Free Online - Combine PDFs Instantly Alternate & Mix Pages
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:alternateMix.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:alternateMix.subtitle">
Upload multiple PDFs and interleave their pages. Drag to reorder the Merge PDFs by alternating pages from each PDF. Preserves Bookmarks.
files, then mix their pages alternately.
</p> </p>
<div <div
+2 -2
View File
@@ -134,13 +134,13 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:backgroundColor.name" data-i18n="tools:backgroundColor.name"
> >
Background Color Free Online - Fast & Secure Background Color
</h1> </h1>
<p <p
class="text-gray-400 mb-6" class="text-gray-400 mb-6"
data-i18n="tools:backgroundColor.subtitle" data-i18n="tools:backgroundColor.subtitle"
> >
Add or change the background color of all pages in your PDF document. Change the background color of your PDF.
</p> </p>
<div <div
+2 -2
View File
@@ -120,10 +120,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:bmpToPdf.name" data-i18n="tools:bmpToPdf.name"
> >
Bmp To Pdf Converter Free Online - Convert Files Fast BMP to PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:bmpToPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:bmpToPdf.subtitle">
Convert one or more BMP images into a single PDF file. Create a PDF from one or more BMP images.
</p> </p>
<div <div
+1 -1
View File
@@ -115,7 +115,7 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:editBookmarks.name" data-i18n="tools:editBookmarks.name"
> >
Bookmark Free Online - Fast & Secure Edit Bookmarks
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:editBookmarks.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:editBookmarks.subtitle">
Add, edit, import, delete and extract PDF bookmarks. Add, edit, import, delete and extract PDF bookmarks.
+1 -1
View File
@@ -120,7 +120,7 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:cbzToPdf.name" data-i18n="tools:cbzToPdf.name"
> >
Cbz To Pdf Converter Free Online - Convert Files Fast CBZ to PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:cbzToPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:cbzToPdf.subtitle">
Convert comic book archives (CBZ/CBR) to PDF format. Supports multiple Convert comic book archives (CBZ/CBR) to PDF format. Supports multiple
+2 -3
View File
@@ -134,14 +134,13 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:changePermissions.name" data-i18n="tools:changePermissions.name"
> >
Change Permissions Free Online - Fast & Secure Change Permissions
</h1> </h1>
<p <p
class="text-gray-400 mb-6" class="text-gray-400 mb-6"
data-i18n="tools:changePermissions.subtitle" data-i18n="tools:changePermissions.subtitle"
> >
Set or change user permissions on a PDF. Modify passwords and access Set or change user permissions on a PDF.
controls.
</p> </p>
<div <div
+2 -3
View File
@@ -134,14 +134,13 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:combineToSinglePage.name" data-i18n="tools:combineToSinglePage.name"
> >
Combine Single Page Free Online - Combine PDFs Instantly Combine to Single Page
</h1> </h1>
<p <p
class="text-gray-400 mb-6" class="text-gray-400 mb-6"
data-i18n="tools:combineToSinglePage.subtitle" data-i18n="tools:combineToSinglePage.subtitle"
> >
Merge all pages of your PDF into one continuous page, either Stitch all pages into one continuous scroll.
horizontally or vertically.
</p> </p>
<div <div
+2 -3
View File
@@ -150,11 +150,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:comparePdfs.name" data-i18n="tools:comparePdfs.name"
> >
Compare Pdfs Free Online - Fast & Secure Compare PDFs
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:comparePdfs.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:comparePdfs.subtitle">
Upload two PDF files to compare them side by side or with overlay Compare two PDFs side by side.
mode.
</p> </p>
<!-- File Upload Areas --> <!-- File Upload Areas -->
+2 -3
View File
@@ -119,11 +119,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:compressPdf.name" data-i18n="tools:compressPdf.name"
> >
Compress PDF Free Online - Reduce File Size Fast Compress PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:compressPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:compressPdf.subtitle">
Reduce file size by choosing the compression method that best suits Reduce the file size of your PDF.
your document. Supports multiple PDFs.
</p> </p>
<div <div
+2 -3
View File
@@ -125,11 +125,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:cropPdf.name" data-i18n="tools:cropPdf.name"
> >
Crop Pdf Free Online - Fast & Secure Crop PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:cropPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:cropPdf.subtitle">
Upload a PDF to visually crop one or more pages. This tool offers a Trim the margins of every page in your PDF.
live preview and two distinct cropping modes.
</p> </p>
<!-- Drop Zone --> <!-- Drop Zone -->
+2 -3
View File
@@ -120,11 +120,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:csvToPdf.name" data-i18n="tools:csvToPdf.name"
> >
Csv To Pdf Converter Free Online - Convert Files Fast CSV to PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:csvToPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:csvToPdf.subtitle">
Convert CSV (Comma-Separated Values) spreadsheet files to PDF format. Convert CSV spreadsheet files to PDF format. Supports multiple files.
Supports multiple files.
</p> </p>
<div <div
+2 -2
View File
@@ -120,10 +120,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:decryptPdf.name" data-i18n="tools:decryptPdf.name"
> >
Decrypt PDF Free - Remove Password Online Decrypt PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:decryptPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:decryptPdf.subtitle">
Unlock password-protected PDF files by removing encryption. Unlock PDF by removing password protection.
</p> </p>
<div <div
+2 -2
View File
@@ -119,10 +119,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:deletePages.name" data-i18n="tools:deletePages.name"
> >
Delete PDF Pages Free - Remove Pages Online Delete Pages
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:deletePages.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:deletePages.subtitle">
Remove specific pages or ranges of pages from your PDF file. Remove specific pages from your document.
</p> </p>
<div <div
id="drop-zone" id="drop-zone"
+2 -3
View File
@@ -108,11 +108,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:deskewPdf.name" data-i18n="tools:deskewPdf.name"
> >
Deskew PDF Free Online - Straighten Tilted Scans Deskew PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:deskewPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:deskewPdf.subtitle">
Automatically detect and correct skewed pages in scanned PDFs. Uses Automatically straighten tilted scanned pages using OpenCV.
advanced image processing to straighten tilted documents.
</p> </p>
<div <div
+2 -3
View File
@@ -120,11 +120,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:dividePages.name" data-i18n="tools:dividePages.name"
> >
Divide Pages Free Online - Fast & Secure Divide Pages
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:dividePages.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:dividePages.subtitle">
Split each page of your PDF horizontally or vertically, turning one Divide pages horizontally or vertically.
page into two.
</p> </p>
<div <div
+2 -2
View File
@@ -134,13 +134,13 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:editAttachments.name" data-i18n="tools:editAttachments.name"
> >
Edit Attachments Free Online - Edit PDFs Securely Edit Attachments
</h1> </h1>
<p <p
class="text-gray-400 mb-6" class="text-gray-400 mb-6"
data-i18n="tools:editAttachments.subtitle" data-i18n="tools:editAttachments.subtitle"
> >
Upload a PDF to view and remove its embedded attachments. View or remove attachments in your PDF.
</p> </p>
<div <div
+2 -3
View File
@@ -120,11 +120,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:editMetadata.name" data-i18n="tools:editMetadata.name"
> >
Edit Metadata Free Online - Edit PDFs Securely Edit Metadata
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:editMetadata.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:editMetadata.subtitle">
Edit metadata fields in your PDF document including title, author, Change the author, title, and other properties.
subject, and keywords.
</p> </p>
<div <div
+1 -1
View File
@@ -123,7 +123,7 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:pdfEditor.name" data-i18n="tools:pdfEditor.name"
> >
Edit PDF Free Online - Modify PDFs Securely PDF Editor
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:pdfEditor.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:pdfEditor.subtitle">
Annotate, highlight, redact, comment, add shapes/images, search, and Annotate, highlight, redact, comment, add shapes/images, search, and
+1 -1
View File
@@ -125,7 +125,7 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:emailToPdf.name" data-i18n="tools:emailToPdf.name"
> >
Email to PDF Converter Free Online - Convert EML MSG Files Email to PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:emailToPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:emailToPdf.subtitle">
Convert email files (EML, MSG) to PDF format. Supports Outlook exports Convert email files (EML, MSG) to PDF format. Supports Outlook exports
+2 -3
View File
@@ -123,11 +123,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:encryptPdf.name" data-i18n="tools:encryptPdf.name"
> >
Encrypt PDF Free - Password Protect PDFs Encrypt PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:encryptPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:encryptPdf.subtitle">
Protect your PDF with 256-bit AES encryption. Add password protection Lock your PDF by adding a password.
and set usage restrictions.
</p> </p>
<div <div
+1 -1
View File
@@ -120,7 +120,7 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:epubToPdf.name" data-i18n="tools:epubToPdf.name"
> >
Epub To Pdf Converter Free Online - Convert Files Fast EPUB to PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:epubToPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:epubToPdf.subtitle">
Convert EPUB e-books to PDF format. Supports multiple files. Convert EPUB e-books to PDF format. Supports multiple files.
+1 -1
View File
@@ -123,7 +123,7 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:excelToPdf.name" data-i18n="tools:excelToPdf.name"
> >
Excel to PDF Converter Free - Convert XLSX Online Excel to PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:excelToPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:excelToPdf.subtitle">
Convert Excel spreadsheets (XLSX, XLS, ODS, CSV) to PDF format. Convert Excel spreadsheets (XLSX, XLS, ODS, CSV) to PDF format.
+2 -3
View File
@@ -134,14 +134,13 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:extractAttachments.name" data-i18n="tools:extractAttachments.name"
> >
Extract Attachments Free Online - Extract PDF Content Extract Attachments
</h1> </h1>
<p <p
class="text-gray-400 mb-6" class="text-gray-400 mb-6"
data-i18n="tools:extractAttachments.subtitle" data-i18n="tools:extractAttachments.subtitle"
> >
Upload PDF files to extract all embedded attachments. Files will be Extract all embedded files from PDF(s) as a ZIP.
downloaded as a ZIP archive.
</p> </p>
<div <div
+2 -3
View File
@@ -126,11 +126,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:extractImages.name" data-i18n="tools:extractImages.name"
> >
Extract Images Free Online - Extract PDF Content Extract Images
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:extractImages.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:extractImages.subtitle">
Extract all embedded images from PDF files. Download individually or Extract all embedded images from your PDF files.
as a ZIP archive.
</p> </p>
<div <div
+2 -3
View File
@@ -120,11 +120,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:extractPages.name" data-i18n="tools:extractPages.name"
> >
Extract PDF Pages Free - Copy Pages Online Extract Pages
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:extractPages.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:extractPages.subtitle">
Extract specific pages from a PDF into separate files. Your files will Save a selection of pages as new files.
download in a ZIP archive.
</p> </p>
<!-- Drop Zone --> <!-- Drop Zone -->
+1 -1
View File
@@ -126,7 +126,7 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:extractTables.name" data-i18n="tools:extractTables.name"
> >
Extract Tables Free Online - Extract PDF Content Extract PDF Tables
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:extractTables.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:extractTables.subtitle">
Extract tables from PDF files and export as CSV, JSON, or Markdown. Extract tables from PDF files and export as CSV, JSON, or Markdown.
+1 -1
View File
@@ -120,7 +120,7 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:fb2ToPdf.name" data-i18n="tools:fb2ToPdf.name"
> >
Fb2 To Pdf Converter Free Online - Convert Files Fast FB2 to PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:fb2ToPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:fb2ToPdf.subtitle">
Convert FictionBook (FB2) e-books to PDF format. Supports multiple Convert FictionBook (FB2) e-books to PDF format. Supports multiple
+2 -3
View File
@@ -120,11 +120,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:fixPageSize.name" data-i18n="tools:fixPageSize.name"
> >
Fix Page Size Free Online - Fast & Secure Fix Page Size
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:fixPageSize.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:fixPageSize.subtitle">
Standardize all pages in your PDF to a uniform size. Choose from Standardize all pages to a uniform size.
standard sizes or set custom dimensions.
</p> </p>
<div <div
+2 -3
View File
@@ -120,11 +120,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:flattenPdf.name" data-i18n="tools:flattenPdf.name"
> >
Flatten Pdf Free Online - Fast & Secure Flatten PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:flattenPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:flattenPdf.subtitle">
Make form fields and annotations non-editable by flattening them into Make form fields and annotations non-editable.
the PDF.
</p> </p>
<div <div
+3 -4
View File
@@ -119,12 +119,11 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:fontToOutline.name" data-i18n="tools:fontToOutline.name"
> >
Font to Outline Free Online - Convert Fonts to Paths Font to Outline
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:fontToOutline.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:fontToOutline.subtitle">
Convert all fonts in your PDF to vector outlines/paths. Ensures Convert all fonts to vector outlines for consistent rendering across
consistent rendering across all devices regardless of font all devices.
availability.
</p> </p>
<div <div
+3 -5
View File
@@ -120,11 +120,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:createPdfForm.name" data-i18n="tools:createPdfForm.name"
> >
Form Creator Free Online - Fast & Secure Create PDF Form
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:createPdfForm.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:createPdfForm.subtitle">
Upload an existing PDF or create a blank PDF to start adding form Create fillable PDF forms with drag-and-drop text fields.
fields.
</p> </p>
<div <div
@@ -262,8 +261,7 @@
Create PDF Form Create PDF Form
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:createPdfForm.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:createPdfForm.subtitle">
Drag and drop fields onto the canvas to create a fillable PDF form. Create fillable PDF forms with drag-and-drop text fields.
Customize field properties and download your form.
</p> </p>
<!-- Main Form Creator Area --> <!-- Main Form Creator Area -->
+1 -1
View File
@@ -120,7 +120,7 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:pdfFormFiller.name" data-i18n="tools:pdfFormFiller.name"
> >
Form Filler Free Online - Fast & Secure PDF Form Filler
</h1> </h1>
<p class="text-gray-400 mb-4"> <p class="text-gray-400 mb-4">
Upload a PDF with form fields. Fill them directly in the viewer below, Upload a PDF with form fields. Fill them directly in the viewer below,
+2 -3
View File
@@ -120,11 +120,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:headerFooter.name" data-i18n="tools:headerFooter.name"
> >
Header Footer Free Online - Fast & Secure Header & Footer
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:headerFooter.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:headerFooter.subtitle">
Add customizable headers and footers to your PDF documents with Add text to the top and bottom of pages.
dynamic page numbers.
</p> </p>
<div <div
+2 -3
View File
@@ -120,11 +120,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:heicToPdf.name" data-i18n="tools:heicToPdf.name"
> >
Heic To Pdf Converter Free Online - Convert Files Fast HEIC to PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:heicToPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:heicToPdf.subtitle">
Convert one or more HEIC (High Efficiency) images from your iPhone or Create a PDF from one or more HEIC images.
camera into a single PDF file.
</p> </p>
<div <div
+3 -2
View File
@@ -121,10 +121,11 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:imageToPdf.name" data-i18n="tools:imageToPdf.name"
> >
Image To Pdf Converter Free Online - Convert Files Fast Images to PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:imageToPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:imageToPdf.subtitle">
Convert one or more images into a single PDF file. Convert JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX,
JP2, PSD, SVG, HEIC, WebP to PDF.
</p> </p>
<div <div
+2 -3
View File
@@ -120,11 +120,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:invertColors.name" data-i18n="tools:invertColors.name"
> >
Invert Colors Free Online - Fast & Secure Invert Colors
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:invertColors.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:invertColors.subtitle">
Invert all colors in your PDF document, creating a negative image Create a "dark mode" version of your PDF.
effect.
</p> </p>
<div <div
+1 -1
View File
@@ -123,7 +123,7 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:jpgToPdf.name" data-i18n="tools:jpgToPdf.name"
> >
JPG to PDF Converter Free - Convert Images Online JPG to PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:jpgToPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:jpgToPdf.subtitle">
Create a PDF from JPG, JPEG, and JPEG2000 (JP2/JPX) images. Create a PDF from JPG, JPEG, and JPEG2000 (JP2/JPX) images.
+2 -3
View File
@@ -112,11 +112,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:jsonToPdf.name" data-i18n="tools:jsonToPdf.name"
> >
Json To Pdf Converter Free Online - Convert Files Fast JSON to PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:jsonToPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:jsonToPdf.subtitle">
Upload multiple JSON files to convert them all to PDF format. Files Convert JSON files to PDF format.
will be downloaded as a ZIP archive.
</p> </p>
<div <div
class="bg-yellow-900/30 border border-yellow-700 rounded-lg p-3 mb-6" class="bg-yellow-900/30 border border-yellow-700 rounded-lg p-3 mb-6"
+2 -3
View File
@@ -120,11 +120,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:linearizePdf.name" data-i18n="tools:linearizePdf.name"
> >
Linearize Pdf Free Online - Fast & Secure Linearize PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:linearizePdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:linearizePdf.subtitle">
Optimize PDF files for fast web viewing. Linearized PDFs load Optimize PDF for fast web viewing.
progressively in browsers.
</p> </p>
<div <div
+3 -3
View File
@@ -130,14 +130,14 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:markdownToPdf.name" data-i18n="tools:markdownToPdf.name"
> >
Markdown To Pdf Converter Free Online - Convert Files Fast Markdown to PDF
</h1> </h1>
<p <p
class="text-gray-400 mb-6" class="text-gray-400 mb-6"
data-i18n="tools:markdownToPdf.subtitle" data-i18n="tools:markdownToPdf.subtitle"
> >
Write markdown with live preview. Supports syntax highlighting, Write or paste Markdown and export it as a beautifully formatted
tables, emoji, and more. Print to save as PDF. PDF.
</p> </p>
<!-- Editor container --> <!-- Editor container -->
+1 -1
View File
@@ -119,7 +119,7 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:mergePdf.name" data-i18n="tools:mergePdf.name"
> >
Merge PDF Files Free - Combine PDFs Instantly Merge PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:mergePdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:mergePdf.subtitle">
Combine whole files, or select specific pages to merge into a new Combine whole files, or select specific pages to merge into a new
+1 -1
View File
@@ -120,7 +120,7 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:mobiToPdf.name" data-i18n="tools:mobiToPdf.name"
> >
Mobi To Pdf Converter Free Online - Convert Files Fast MOBI to PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:mobiToPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:mobiToPdf.subtitle">
Convert MOBI e-books to PDF format. Supports multiple files. Convert MOBI e-books to PDF format. Supports multiple files.
+2 -3
View File
@@ -120,11 +120,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:nUpPdf.name" data-i18n="tools:nUpPdf.name"
> >
N Up Pdf Free Online - Fast & Secure N-Up PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:nUpPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:nUpPdf.subtitle">
Arrange multiple pages onto a single sheet. Perfect for printing Arrange multiple pages onto a single sheet.
handouts or saving paper.
</p> </p>
<div <div
+2 -3
View File
@@ -123,11 +123,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:ocrPdf.name" data-i18n="tools:ocrPdf.name"
> >
OCR PDF Free Online - Extract Text from Scans OCR PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:ocrPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:ocrPdf.subtitle">
Convert scanned PDFs into searchable documents. Select one or more Make a PDF searchable and copyable.
languages present in your file for the best results.
</p> </p>
<!-- How it works info box --> <!-- How it works info box -->
+1 -1
View File
@@ -120,7 +120,7 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:odgToPdf.name" data-i18n="tools:odgToPdf.name"
> >
Odg To Pdf Converter Free Online - Convert Files Fast ODG to PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:odgToPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:odgToPdf.subtitle">
Convert OpenDocument Graphics (ODG) files to PDF format. Supports Convert OpenDocument Graphics (ODG) files to PDF format. Supports
+1 -1
View File
@@ -122,7 +122,7 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:odpToPdf.name" data-i18n="tools:odpToPdf.name"
> >
Odp To Pdf Converter Free Online - Convert Files Fast ODP to PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:odpToPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:odpToPdf.subtitle">
Convert OpenDocument Presentation (ODP) files to PDF format. Supports Convert OpenDocument Presentation (ODP) files to PDF format. Supports
+1 -1
View File
@@ -122,7 +122,7 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:odsToPdf.name" data-i18n="tools:odsToPdf.name"
> >
Ods To Pdf Converter Free Online - Convert Files Fast ODS to PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:odsToPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:odsToPdf.subtitle">
Convert OpenDocument Spreadsheet (ODS) files to PDF format. Supports Convert OpenDocument Spreadsheet (ODS) files to PDF format. Supports
+3 -3
View File
@@ -120,11 +120,11 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:odtToPdf.name" data-i18n="tools:odtToPdf.name"
> >
Odt To Pdf Converter Free Online - Convert Files Fast ODT to PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:odtToPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:odtToPdf.subtitle">
Convert OpenDocument Text (.odt) files to PDF format. Supports Convert OpenDocument Text files to PDF format. Supports multiple
multiple files. files.
</p> </p>
<div <div
+11 -20
View File
@@ -119,23 +119,13 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:duplicateOrganize.name" data-i18n="tools:duplicateOrganize.name"
> >
Organize PDF Pages Free - Reorder PDFs Online Duplicate & Organize
</h1> </h1>
<p <p
class="text-gray-400 mb-6" class="text-gray-400 mb-6"
data-i18n="tools:duplicateOrganize.subtitle" data-i18n="tools:duplicateOrganize.subtitle"
> >
Drag pages to reorder them. Use the Duplicate, reorder, and delete pages.
<i
data-lucide="copy-plus"
class="inline-block w-4 h-4 mx-1 align-text-bottom text-green-500"
></i>
icon to duplicate a page or the
<i
data-lucide="x-circle"
class="inline-block w-4 h-4 mx-1 align-text-bottom text-red-400"
></i>
icon to delete it.
</p> </p>
<!-- Drop Zone --> <!-- Drop Zone -->
@@ -182,24 +172,25 @@
></div> ></div>
<!-- Advanced Settings --> <!-- Advanced Settings -->
<div id="advanced-settings" class="hidden mt-6"> <div id="advanced-settings" class="hidden mt-6 space-y-3">
<h3 class="text-lg font-semibold text-white mb-2"> <h3 class="text-lg font-semibold text-white">Advanced Settings</h3>
Advanced Settings <div class="bg-gray-800/50 p-4 rounded-lg border border-gray-700">
</h3>
<div class="bg-gray-700 p-4 rounded-lg">
<label <label
for="page-order-input" for="page-order-input"
class="block text-sm font-medium text-gray-300 mb-2" class="block text-sm font-medium text-gray-300 mb-2"
>Page Order (comma-separated)</label >Page Order (comma-separated)</label
> >
<div class="flex gap-2"> <div class="flex gap-3">
<input <input
type="text" type="text"
id="page-order-input" id="page-order-input"
class="w-full bg-gray-900 text-white rounded-md px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 border border-gray-600" class="flex-1 bg-gray-900 text-white rounded-lg px-3 py-2.5 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 border border-gray-600 placeholder-gray-500"
placeholder="e.g., 3,1,4,2" placeholder="e.g., 3,1,4,2"
/> />
<button id="apply-order-btn" class="btn-secondary"> <button
id="apply-order-btn"
class="btn-gradient whitespace-nowrap !px-5 !py-2.5 text-sm"
>
Apply Order Apply Order
</button> </button>
</div> </div>
+2 -3
View File
@@ -132,11 +132,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:pageDimensions.name" data-i18n="tools:pageDimensions.name"
> >
Page Dimensions Free Online - Fast & Secure Page Dimensions
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:pageDimensions.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:pageDimensions.subtitle">
Analyze PDF page dimensions and sizes. View detailed information about Analyze page size, orientation, and units.
each page.
</p> </p>
<div <div
+2 -3
View File
@@ -120,11 +120,10 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:pageNumbers.name" data-i18n="tools:pageNumbers.name"
> >
Page Numbers Free Online - Fast & Secure Page Numbers
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:pageNumbers.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:pageNumbers.subtitle">
Add page numbers to your PDF documents with customizable position, Insert page numbers into your document.
format, and styling.
</p> </p>
<div <div
+1 -1
View File
@@ -120,7 +120,7 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:pagesToPdf.name" data-i18n="tools:pagesToPdf.name"
> >
Pages To Pdf Converter Free Online - Convert Files Fast Pages to PDF
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:pagesToPdf.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:pagesToPdf.subtitle">
Convert Apple Pages documents to PDF format. Supports multiple files. Convert Apple Pages documents to PDF format. Supports multiple files.
+1 -1
View File
@@ -120,7 +120,7 @@
class="text-2xl font-bold text-white mb-2" class="text-2xl font-bold text-white mb-2"
data-i18n="tools:pdfBooklet.name" data-i18n="tools:pdfBooklet.name"
> >
Pdf Booklet Free Online - Fast & Secure PDF Booklet
</h1> </h1>
<p class="text-gray-400 mb-6" data-i18n="tools:pdfBooklet.subtitle"> <p class="text-gray-400 mb-6" data-i18n="tools:pdfBooklet.subtitle">
Rearrange pages for double-sided booklet printing. Fold and staple to Rearrange pages for double-sided booklet printing. Fold and staple to

Some files were not shown because too many files have changed in this diff Show More