mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-07 16:41:00 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d9ffe2997 | ||
|
|
d32251014e | ||
|
|
c2147a2f20 | ||
|
|
90998edb25 | ||
|
|
84848ab902 | ||
|
|
2aaea50031 | ||
|
|
5fbf48601e | ||
|
|
4cec7bbe58 | ||
|
|
e505c70b2f | ||
|
|
841428f2e2 | ||
|
|
957b78fafd | ||
|
|
34f12655c1 | ||
|
|
74d7af283e | ||
|
|
5ef73edea2 | ||
|
|
9b099c6a6d |
+5
-6
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Generated
+795
-1133
File diff suppressed because it is too large
Load Diff
+29
-29
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -163,7 +163,8 @@
|
|||||||
},
|
},
|
||||||
"removeBlankPages": {
|
"removeBlankPages": {
|
||||||
"name": "إزالة الصفحات الفارغة",
|
"name": "إزالة الصفحات الفارغة",
|
||||||
"subtitle": "اكتشاف وحذف الصفحات الفارغة تلقائيًا."
|
"subtitle": "اكتشاف وحذف الصفحات الفارغة تلقائيًا.",
|
||||||
|
"sensitivityHint": "أعلى = أكثر صرامة، فقط الصفحات الفارغة تمامًا. أقل = يسمح بصفحات تحتوي على بعض المحتوى."
|
||||||
},
|
},
|
||||||
"imageToPdf": {
|
"imageToPdf": {
|
||||||
"name": "صور إلى PDF",
|
"name": "صور إلى PDF",
|
||||||
|
|||||||
@@ -163,7 +163,8 @@
|
|||||||
},
|
},
|
||||||
"removeBlankPages": {
|
"removeBlankPages": {
|
||||||
"name": "Выдаліць пустыя старонкі",
|
"name": "Выдаліць пустыя старонкі",
|
||||||
"subtitle": "Аўтаматычна выявіць і выдаліць пустыя старонкі."
|
"subtitle": "Аўтаматычна выявіць і выдаліць пустыя старонкі.",
|
||||||
|
"sensitivityHint": "Вышэй = больш строга, толькі цалкам пустыя старонкі. Ніжэй = дапускае старонкі з нейкім змесцівам."
|
||||||
},
|
},
|
||||||
"imageToPdf": {
|
"imageToPdf": {
|
||||||
"name": "Відарысы ў PDF",
|
"name": "Відарысы ў PDF",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -163,7 +163,8 @@
|
|||||||
},
|
},
|
||||||
"removeBlankPages": {
|
"removeBlankPages": {
|
||||||
"name": "移除空白頁面",
|
"name": "移除空白頁面",
|
||||||
"subtitle": "自動偵測並刪除空白頁面。"
|
"subtitle": "自動偵測並刪除空白頁面。",
|
||||||
|
"sensitivityHint": "越高 = 越嚴格,僅偵測純空白頁面。越低 = 允許包含少量內容的頁面。"
|
||||||
},
|
},
|
||||||
"imageToPdf": {
|
"imageToPdf": {
|
||||||
"name": "圖片轉 PDF",
|
"name": "圖片轉 PDF",
|
||||||
|
|||||||
@@ -163,7 +163,8 @@
|
|||||||
},
|
},
|
||||||
"removeBlankPages": {
|
"removeBlankPages": {
|
||||||
"name": "移除空白页",
|
"name": "移除空白页",
|
||||||
"subtitle": "自动检测并删除空白页。"
|
"subtitle": "自动检测并删除空白页。",
|
||||||
|
"sensitivityHint": "越高 = 越严格,仅检测纯空白页。越低 = 允许包含少量内容的页面。"
|
||||||
},
|
},
|
||||||
"imageToPdf": {
|
"imageToPdf": {
|
||||||
"name": "图片转 PDF",
|
"name": "图片转 PDF",
|
||||||
|
|||||||
@@ -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
@@ -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),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|||||||
@@ -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
@@ -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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
@@ -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 = '';
|
||||||
}
|
}
|
||||||
|
|||||||
+536
-431
File diff suppressed because it is too large
Load Diff
@@ -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> = {};
|
||||||
|
|||||||
@@ -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';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.');
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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
@@ -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 = '×';
|
|
||||||
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 = '×';
|
||||||
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">
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
|||||||
@@ -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],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user