mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-08 16:57:14 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53bcf09566 | ||
|
|
d2dc459ba9 | ||
|
|
71ced499e7 | ||
|
|
fc264f796a | ||
|
|
e55c2ed405 | ||
|
|
4b875a638c | ||
|
|
b71d6c9b96 | ||
|
|
d63d1683c7 | ||
|
|
0347013df1 | ||
|
|
412f2f1acb | ||
|
|
a471bbcf4d | ||
|
|
e1843e17b3 | ||
|
|
ac3ad68b56 | ||
|
|
745698a5ba | ||
|
|
dc1dbb036c | ||
|
|
175c52bb1f | ||
|
|
1d9b7c2a07 | ||
|
|
1534ce4d85 | ||
|
|
3cab1d37c7 |
No files matched your search
@@ -13,10 +13,6 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
### FOSS Hack 2026
|
|
||||||
|
|
||||||
[](https://drive.google.com/file/d/14Vf62PvHiuf1RKFKtMlAzpbf4QQPLpRF/view?usp=sharing)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|||||||
+15
-1
@@ -33,6 +33,13 @@ BentoPDF uses **i18next** for internationalization (i18n). Currently supported l
|
|||||||
- **Traditional Chinese (Taiwan)** (`zh-TW`)
|
- **Traditional Chinese (Taiwan)** (`zh-TW`)
|
||||||
- **Korean** (`ko`)
|
- **Korean** (`ko`)
|
||||||
- **Russian** (`ru`)
|
- **Russian** (`ru`)
|
||||||
|
- **Ukrainian** (`uk`)
|
||||||
|
- **Slovak** (`sk`)
|
||||||
|
- **Arabic** (`ar`)
|
||||||
|
- **Dutch** (`nl`)
|
||||||
|
- **Danish** (`da`)
|
||||||
|
- **Swedish** (`sv`)
|
||||||
|
- **Japanese** (`ja`)
|
||||||
|
|
||||||
The app automatically detects the language from the URL path:
|
The app automatically detects the language from the URL path:
|
||||||
|
|
||||||
@@ -603,8 +610,15 @@ Current translation coverage:
|
|||||||
| Traditional Chinese | `zh-TW` | ✅ Complete | Community |
|
| Traditional Chinese | `zh-TW` | ✅ Complete | Community |
|
||||||
| Korean | `ko` | ✅ Complete | Community |
|
| Korean | `ko` | ✅ Complete | Community |
|
||||||
| Russian | `ru` | ✅ Complete | Community |
|
| Russian | `ru` | ✅ Complete | Community |
|
||||||
|
| Ukrainian | `uk` | ✅ Complete | Community |
|
||||||
|
| Slovak | `sk` | ✅ Complete | Community |
|
||||||
|
| Arabic | `ar` | ✅ Complete | Community |
|
||||||
|
| Dutch | `nl` | ✅ Complete | Community |
|
||||||
|
| Danish | `da` | ✅ Complete | Community |
|
||||||
|
| Swedish | `sv` | ✅ Complete | Community |
|
||||||
|
| Japanese | `ja` | ✅ Complete | Community |
|
||||||
| Your Language | `??` | 🚧 In Progress | You? |
|
| Your Language | `??` | 🚧 In Progress | You? |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Last Updated**: January 2026
|
**Last Updated**: June 2026
|
||||||
+2
-2
@@ -7,10 +7,10 @@ type: application
|
|||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
# to the chart and its templates, including the app version.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
version: 1.0.4
|
version: 1.0.5
|
||||||
|
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# This is the version number of the application being deployed. This version number should be
|
||||||
# incremented each time you make changes to the application. Versions are not expected to
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
# It is recommended to use it with quotes.
|
# It is recommended to use it with quotes.
|
||||||
appVersion: '2.8.5'
|
appVersion: '2.8.6'
|
||||||
Generated
+84
-84
@@ -104,7 +104,7 @@
|
|||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.56.1",
|
"typescript-eslint": "^8.56.1",
|
||||||
"vite": "^8.0.9",
|
"vite": "^8.0.10",
|
||||||
"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.26.0",
|
"vite-plugin-node-polyfills": "^0.26.0",
|
||||||
@@ -1284,9 +1284,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.9.2",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||||
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
|
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1302,9 +1302,9 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.9.2",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||||
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
|
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2049,9 +2049,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.126.0",
|
"version": "0.127.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
|
||||||
"integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==",
|
"integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/Boshen"
|
"url": "https://github.com/sponsors/Boshen"
|
||||||
@@ -2120,9 +2120,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==",
|
"integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2136,9 +2136,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==",
|
"integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2152,9 +2152,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==",
|
"integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2168,9 +2168,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==",
|
"integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2184,9 +2184,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==",
|
"integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -2200,9 +2200,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==",
|
"integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2216,9 +2216,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==",
|
"integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2232,9 +2232,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==",
|
"integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -2248,9 +2248,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==",
|
"integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -2264,9 +2264,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==",
|
"integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2280,9 +2280,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==",
|
"integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2296,9 +2296,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==",
|
"integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2312,17 +2312,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==",
|
"integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/core": "1.9.2",
|
"@emnapi/core": "1.10.0",
|
||||||
"@emnapi/runtime": "1.9.2",
|
"@emnapi/runtime": "1.10.0",
|
||||||
"@napi-rs/wasm-runtime": "^1.1.4"
|
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2330,9 +2330,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==",
|
"integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2346,9 +2346,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==",
|
"integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2362,9 +2362,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==",
|
"integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/plugin-inject": {
|
"node_modules/@rollup/plugin-inject": {
|
||||||
@@ -10649,13 +10649,13 @@
|
|||||||
"license": "Unlicense"
|
"license": "Unlicense"
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.0-rc.16",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
|
||||||
"integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==",
|
"integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.126.0",
|
"@oxc-project/types": "=0.127.0",
|
||||||
"@rolldown/pluginutils": "1.0.0-rc.16"
|
"@rolldown/pluginutils": "1.0.0-rc.17"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"rolldown": "bin/cli.mjs"
|
"rolldown": "bin/cli.mjs"
|
||||||
@@ -10664,21 +10664,21 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm64": "1.0.0-rc.16",
|
"@rolldown/binding-android-arm64": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.16",
|
"@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.16",
|
"@rolldown/binding-darwin-x64": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.16",
|
"@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.16",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.16",
|
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.16",
|
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.16",
|
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.16",
|
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.16",
|
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.16",
|
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.16",
|
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.16",
|
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.16",
|
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16"
|
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
@@ -12113,15 +12113,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.9",
|
"version": "8.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
|
||||||
"integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==",
|
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
"postcss": "^8.5.10",
|
"postcss": "^8.5.10",
|
||||||
"rolldown": "1.0.0-rc.16",
|
"rolldown": "1.0.0-rc.17",
|
||||||
"tinyglobby": "^0.2.16"
|
"tinyglobby": "^0.2.16"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "bento-pdf",
|
"name": "bento-pdf",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.8.5",
|
"version": "2.8.6",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.56.1",
|
"typescript-eslint": "^8.56.1",
|
||||||
"vite": "^8.0.9",
|
"vite": "^8.0.10",
|
||||||
"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.26.0",
|
"vite-plugin-node-polyfills": "^0.26.0",
|
||||||
|
|||||||
@@ -350,9 +350,12 @@
|
|||||||
"download": "تنزيل:",
|
"download": "تنزيل:",
|
||||||
"downloadSelected": "تنزيل المحدد",
|
"downloadSelected": "تنزيل المحدد",
|
||||||
"exportPdf": "تصدير PDF",
|
"exportPdf": "تصدير PDF",
|
||||||
"uploadPdfFiles": "اختر ملفات PDF",
|
"uploadPdfFiles": "اختر ملفات PDF أو صور",
|
||||||
"dragAndDrop": "اسحب وأفلت ملفات PDF هنا، أو انقر للاختيار",
|
"dragAndDrop": "اسحب وأفلت ملفات PDF أو الصور هنا، أو انقر للاختيار",
|
||||||
"selectFiles": "اختر الملفات",
|
"selectFiles": "اختر الملفات",
|
||||||
|
"dropToAdd": "أفلت ملفات PDF أو الصور للإضافة",
|
||||||
|
"convertingImages": "جارٍ تحويل الصور إلى PDF...",
|
||||||
|
"failedToConvertImages": "فشل تحويل الصور إلى PDF.",
|
||||||
"renderingPages": "جارٍ عرض الصفحات...",
|
"renderingPages": "جارٍ عرض الصفحات...",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "تكرار هذه الصفحة",
|
"duplicatePage": "تكرار هذه الصفحة",
|
||||||
|
|||||||
@@ -350,9 +350,12 @@
|
|||||||
"download": "Спампаваць:",
|
"download": "Спампаваць:",
|
||||||
"downloadSelected": "Спампаваць выбранае",
|
"downloadSelected": "Спампаваць выбранае",
|
||||||
"exportPdf": "Экспартаваць PDF",
|
"exportPdf": "Экспартаваць PDF",
|
||||||
"uploadPdfFiles": "Выбраць PDF файлы",
|
"uploadPdfFiles": "Выбраць PDF або выявы",
|
||||||
"dragAndDrop": "Перацягніце PDF файлы сюды або націсніце, каб выбраць",
|
"dragAndDrop": "Перацягніце PDF або выявы сюды або націсніце, каб выбраць",
|
||||||
"selectFiles": "Выбраць файлы",
|
"selectFiles": "Выбраць файлы",
|
||||||
|
"dropToAdd": "Адпусціце PDF або выявы для дабаўлення",
|
||||||
|
"convertingImages": "Канвертацыя выяў у PDF...",
|
||||||
|
"failedToConvertImages": "Не ўдалося канвертаваць выявы ў PDF.",
|
||||||
"renderingPages": "Апрацоўка старонак...",
|
"renderingPages": "Апрацоўка старонак...",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "Дубляваць гэту старонку",
|
"duplicatePage": "Дубляваць гэту старонку",
|
||||||
|
|||||||
@@ -350,9 +350,12 @@
|
|||||||
"download": "Download:",
|
"download": "Download:",
|
||||||
"downloadSelected": "Download valgte",
|
"downloadSelected": "Download valgte",
|
||||||
"exportPdf": "Eksportér PDF",
|
"exportPdf": "Eksportér PDF",
|
||||||
"uploadPdfFiles": "Vælg PDF-filer",
|
"uploadPdfFiles": "Vælg PDF- eller billedfiler",
|
||||||
"dragAndDrop": "Træk og slip PDF-filer her, eller klik for at vælge",
|
"dragAndDrop": "Træk og slip PDF- eller billedfiler her, eller klik for at vælge",
|
||||||
"selectFiles": "Vælg filer",
|
"selectFiles": "Vælg filer",
|
||||||
|
"dropToAdd": "Slip PDF- eller billedfiler for at tilføje",
|
||||||
|
"convertingImages": "Konverterer billeder til PDF...",
|
||||||
|
"failedToConvertImages": "Kunne ikke konvertere billeder til PDF.",
|
||||||
"renderingPages": "Renderer sider...",
|
"renderingPages": "Renderer sider...",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "Duplikér denne side",
|
"duplicatePage": "Duplikér denne side",
|
||||||
|
|||||||
@@ -350,9 +350,12 @@
|
|||||||
"download": "Download:",
|
"download": "Download:",
|
||||||
"downloadSelected": "Auswahl herunterladen",
|
"downloadSelected": "Auswahl herunterladen",
|
||||||
"exportPdf": "PDF exportieren",
|
"exportPdf": "PDF exportieren",
|
||||||
"uploadPdfFiles": "PDF-Dateien auswählen",
|
"uploadPdfFiles": "PDF- oder Bilddateien auswählen",
|
||||||
"dragAndDrop": "PDF-Dateien hierhin ziehen oder klicken zum Auswählen",
|
"dragAndDrop": "PDF- oder Bilddateien hierhin ziehen oder klicken zum Auswählen",
|
||||||
"selectFiles": "Dateien auswählen",
|
"selectFiles": "Dateien auswählen",
|
||||||
|
"dropToAdd": "PDF- oder Bilddateien zum Hinzufügen ablegen",
|
||||||
|
"convertingImages": "Bilder werden in PDF konvertiert...",
|
||||||
|
"failedToConvertImages": "Bilder konnten nicht in PDF konvertiert werden.",
|
||||||
"renderingPages": "Seiten werden gerendert...",
|
"renderingPages": "Seiten werden gerendert...",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "Diese Seite duplizieren",
|
"duplicatePage": "Diese Seite duplizieren",
|
||||||
|
|||||||
@@ -350,9 +350,12 @@
|
|||||||
"download": "Download:",
|
"download": "Download:",
|
||||||
"downloadSelected": "Download Selected",
|
"downloadSelected": "Download Selected",
|
||||||
"exportPdf": "Export PDF",
|
"exportPdf": "Export PDF",
|
||||||
"uploadPdfFiles": "Select PDF Files",
|
"uploadPdfFiles": "Select PDF or image files",
|
||||||
"dragAndDrop": "Drag and drop PDF files here, or click to select",
|
"dragAndDrop": "Drag and drop PDF or image files here, or click to select",
|
||||||
"selectFiles": "Select Files",
|
"selectFiles": "Select Files",
|
||||||
|
"dropToAdd": "Drop PDF or image files to add",
|
||||||
|
"convertingImages": "Converting images to PDF...",
|
||||||
|
"failedToConvertImages": "Failed to convert images to PDF.",
|
||||||
"renderingPages": "Rendering pages...",
|
"renderingPages": "Rendering pages...",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "Duplicate this page",
|
"duplicatePage": "Duplicate this page",
|
||||||
|
|||||||
@@ -350,9 +350,12 @@
|
|||||||
"download": "Descargar:",
|
"download": "Descargar:",
|
||||||
"downloadSelected": "Descargar Seleccionados",
|
"downloadSelected": "Descargar Seleccionados",
|
||||||
"exportPdf": "Exportar PDF",
|
"exportPdf": "Exportar PDF",
|
||||||
"uploadPdfFiles": "Seleccionar Archivos PDF",
|
"uploadPdfFiles": "Seleccionar archivos PDF o imágenes",
|
||||||
"dragAndDrop": "Arrastra y suelta archivos PDF aquí, o haz clic para seleccionar",
|
"dragAndDrop": "Arrastra y suelta archivos PDF o imágenes aquí, o haz clic para seleccionar",
|
||||||
"selectFiles": "Seleccionar Archivos",
|
"selectFiles": "Seleccionar Archivos",
|
||||||
|
"dropToAdd": "Suelta archivos PDF o imágenes para añadir",
|
||||||
|
"convertingImages": "Convirtiendo imágenes a PDF...",
|
||||||
|
"failedToConvertImages": "No se pudieron convertir las imágenes a PDF.",
|
||||||
"renderingPages": "Renderizando páginas...",
|
"renderingPages": "Renderizando páginas...",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "Duplicar esta página",
|
"duplicatePage": "Duplicar esta página",
|
||||||
|
|||||||
@@ -350,9 +350,12 @@
|
|||||||
"download": "Téléchargement :",
|
"download": "Téléchargement :",
|
||||||
"downloadSelected": "Télécharger la sélection",
|
"downloadSelected": "Télécharger la sélection",
|
||||||
"exportPdf": "Exporter en PDF",
|
"exportPdf": "Exporter en PDF",
|
||||||
"uploadPdfFiles": "Sélectionner des fichiers PDF",
|
"uploadPdfFiles": "Sélectionner des fichiers PDF ou des images",
|
||||||
"dragAndDrop": "Glissez-déposez vos fichiers PDF ici ou cliquez pour sélectionner",
|
"dragAndDrop": "Glissez-déposez vos fichiers PDF ou images ici, ou cliquez pour sélectionner",
|
||||||
"selectFiles": "Sélectionner des fichiers",
|
"selectFiles": "Sélectionner des fichiers",
|
||||||
|
"dropToAdd": "Déposez des fichiers PDF ou images à ajouter",
|
||||||
|
"convertingImages": "Conversion des images en PDF...",
|
||||||
|
"failedToConvertImages": "Échec de la conversion des images en PDF.",
|
||||||
"renderingPages": "Rendu des pages...",
|
"renderingPages": "Rendu des pages...",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "Dupliquer cette page",
|
"duplicatePage": "Dupliquer cette page",
|
||||||
|
|||||||
@@ -350,9 +350,12 @@
|
|||||||
"download": "Unduh:",
|
"download": "Unduh:",
|
||||||
"downloadSelected": "Unduh yang Dipilih",
|
"downloadSelected": "Unduh yang Dipilih",
|
||||||
"exportPdf": "Ekspor PDF",
|
"exportPdf": "Ekspor PDF",
|
||||||
"uploadPdfFiles": "Pilih File PDF",
|
"uploadPdfFiles": "Pilih File PDF atau Gambar",
|
||||||
"dragAndDrop": "Seret dan jatuhkan file PDF di sini, atau klik untuk memilih",
|
"dragAndDrop": "Seret dan jatuhkan file PDF atau gambar di sini, atau klik untuk memilih",
|
||||||
"selectFiles": "Pilih File",
|
"selectFiles": "Pilih File",
|
||||||
|
"dropToAdd": "Jatuhkan file PDF atau gambar untuk menambahkan",
|
||||||
|
"convertingImages": "Mengonversi gambar ke PDF...",
|
||||||
|
"failedToConvertImages": "Gagal mengonversi gambar ke PDF.",
|
||||||
"renderingPages": "Merender halaman...",
|
"renderingPages": "Merender halaman...",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "Duplikat halaman ini",
|
"duplicatePage": "Duplikat halaman ini",
|
||||||
|
|||||||
@@ -350,9 +350,12 @@
|
|||||||
"download": "Scarica:",
|
"download": "Scarica:",
|
||||||
"downloadSelected": "Scarica selezionati",
|
"downloadSelected": "Scarica selezionati",
|
||||||
"exportPdf": "Esporta PDF",
|
"exportPdf": "Esporta PDF",
|
||||||
"uploadPdfFiles": "Seleziona file PDF",
|
"uploadPdfFiles": "Seleziona file PDF o immagini",
|
||||||
"dragAndDrop": "Trascina e rilascia i file PDF qui, oppure clicca per selezionare",
|
"dragAndDrop": "Trascina e rilascia file PDF o immagini qui, oppure clicca per selezionare",
|
||||||
"selectFiles": "Seleziona file",
|
"selectFiles": "Seleziona file",
|
||||||
|
"dropToAdd": "Rilascia file PDF o immagini per aggiungere",
|
||||||
|
"convertingImages": "Conversione delle immagini in PDF...",
|
||||||
|
"failedToConvertImages": "Impossibile convertire le immagini in PDF.",
|
||||||
"renderingPages": "Generazione anteprime pagine...",
|
"renderingPages": "Generazione anteprime pagine...",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "Duplica questa pagina",
|
"duplicatePage": "Duplica questa pagina",
|
||||||
|
|||||||
@@ -350,9 +350,12 @@
|
|||||||
"download": "ダウンロード:",
|
"download": "ダウンロード:",
|
||||||
"downloadSelected": "選択したものをダウンロード",
|
"downloadSelected": "選択したものをダウンロード",
|
||||||
"exportPdf": "PDFをエクスポート",
|
"exportPdf": "PDFをエクスポート",
|
||||||
"uploadPdfFiles": "PDFファイルを選択",
|
"uploadPdfFiles": "PDFまたは画像ファイルを選択",
|
||||||
"dragAndDrop": "ここにPDFファイルをドラッグ&ドロップするかクリックして選択します",
|
"dragAndDrop": "ここにPDFまたは画像ファイルをドラッグ&ドロップするかクリックして選択します",
|
||||||
"selectFiles": "ファイルの選択",
|
"selectFiles": "ファイルの選択",
|
||||||
|
"dropToAdd": "PDFまたは画像ファイルをドロップして追加",
|
||||||
|
"convertingImages": "画像をPDFに変換中...",
|
||||||
|
"failedToConvertImages": "画像をPDFに変換できませんでした。",
|
||||||
"renderingPages": "ページをレンダリング中...",
|
"renderingPages": "ページをレンダリング中...",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "このページを複製",
|
"duplicatePage": "このページを複製",
|
||||||
|
|||||||
@@ -350,9 +350,12 @@
|
|||||||
"download": "다운로드:",
|
"download": "다운로드:",
|
||||||
"downloadSelected": "선택 항목 다운로드",
|
"downloadSelected": "선택 항목 다운로드",
|
||||||
"exportPdf": "PDF 내보내기",
|
"exportPdf": "PDF 내보내기",
|
||||||
"uploadPdfFiles": "PDF 파일 선택",
|
"uploadPdfFiles": "PDF 또는 이미지 파일 선택",
|
||||||
"dragAndDrop": "PDF 파일을 여기에 끌어다 놓거나 클릭하여 선택하세요",
|
"dragAndDrop": "PDF 또는 이미지 파일을 여기에 끌어다 놓거나 클릭하여 선택하세요",
|
||||||
"selectFiles": "파일 선택",
|
"selectFiles": "파일 선택",
|
||||||
|
"dropToAdd": "PDF 또는 이미지 파일을 놓아 추가",
|
||||||
|
"convertingImages": "이미지를 PDF로 변환 중...",
|
||||||
|
"failedToConvertImages": "이미지를 PDF로 변환하지 못했습니다.",
|
||||||
"renderingPages": "페이지 렌더링 중...",
|
"renderingPages": "페이지 렌더링 중...",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "이 페이지 복제",
|
"duplicatePage": "이 페이지 복제",
|
||||||
|
|||||||
@@ -350,9 +350,12 @@
|
|||||||
"download": "Laden:",
|
"download": "Laden:",
|
||||||
"downloadSelected": "Selectie laden",
|
"downloadSelected": "Selectie laden",
|
||||||
"exportPdf": "PDF exporteren",
|
"exportPdf": "PDF exporteren",
|
||||||
"uploadPdfFiles": "PDF-bestanden selecteren",
|
"uploadPdfFiles": "PDF- of afbeeldingsbestanden selecteren",
|
||||||
"dragAndDrop": "Klik om een bestand te selecteren, of sleep er een hierheen",
|
"dragAndDrop": "Klik om een bestand te selecteren, of sleep een PDF of afbeelding hierheen",
|
||||||
"selectFiles": "Bestanden selecteren",
|
"selectFiles": "Bestanden selecteren",
|
||||||
|
"dropToAdd": "Sleep PDF- of afbeeldingsbestanden om toe te voegen",
|
||||||
|
"convertingImages": "Afbeeldingen omzetten naar PDF...",
|
||||||
|
"failedToConvertImages": "Afbeeldingen omzetten naar PDF mislukt.",
|
||||||
"renderingPages": "Pagina's verwerken…",
|
"renderingPages": "Pagina's verwerken…",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "Deze pagina dupliceren",
|
"duplicatePage": "Deze pagina dupliceren",
|
||||||
|
|||||||
@@ -350,9 +350,12 @@
|
|||||||
"download": "Baixar:",
|
"download": "Baixar:",
|
||||||
"downloadSelected": "Baixar Selecionadas",
|
"downloadSelected": "Baixar Selecionadas",
|
||||||
"exportPdf": "Exportar PDF",
|
"exportPdf": "Exportar PDF",
|
||||||
"uploadPdfFiles": "Selecionar Arquivos PDF",
|
"uploadPdfFiles": "Selecionar Arquivos PDF ou Imagens",
|
||||||
"dragAndDrop": "Arraste arquivos PDF aqui ou clique para selecionar",
|
"dragAndDrop": "Arraste arquivos PDF ou imagens aqui ou clique para selecionar",
|
||||||
"selectFiles": "Selecionar Arquivos",
|
"selectFiles": "Selecionar Arquivos",
|
||||||
|
"dropToAdd": "Solte arquivos PDF ou imagens para adicionar",
|
||||||
|
"convertingImages": "Convertendo imagens em PDF...",
|
||||||
|
"failedToConvertImages": "Falha ao converter imagens em PDF.",
|
||||||
"renderingPages": "Renderizando páginas...",
|
"renderingPages": "Renderizando páginas...",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "Duplicar esta página",
|
"duplicatePage": "Duplicar esta página",
|
||||||
|
|||||||
@@ -350,9 +350,12 @@
|
|||||||
"download": "Скачать:",
|
"download": "Скачать:",
|
||||||
"downloadSelected": "Скачать выбранное",
|
"downloadSelected": "Скачать выбранное",
|
||||||
"exportPdf": "Экспортировать PDF",
|
"exportPdf": "Экспортировать PDF",
|
||||||
"uploadPdfFiles": "Выбрать PDF-файлы",
|
"uploadPdfFiles": "Выбрать PDF-файлы или изображения",
|
||||||
"dragAndDrop": "Перетащите PDF-файлы сюда или нажмите, чтобы выбрать",
|
"dragAndDrop": "Перетащите PDF-файлы или изображения сюда или нажмите, чтобы выбрать",
|
||||||
"selectFiles": "Выбрать файлы",
|
"selectFiles": "Выбрать файлы",
|
||||||
|
"dropToAdd": "Отпустите PDF-файлы или изображения, чтобы добавить",
|
||||||
|
"convertingImages": "Преобразование изображений в PDF...",
|
||||||
|
"failedToConvertImages": "Не удалось преобразовать изображения в PDF.",
|
||||||
"renderingPages": "Отрисовка страниц...",
|
"renderingPages": "Отрисовка страниц...",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "Дублировать эту страницу",
|
"duplicatePage": "Дублировать эту страницу",
|
||||||
|
|||||||
@@ -0,0 +1,387 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"home": "Domov",
|
||||||
|
"about": "O nás",
|
||||||
|
"contact": "Kontakt",
|
||||||
|
"licensing": "Licencie",
|
||||||
|
"allTools": "Všetky nástroje",
|
||||||
|
"docs": "Dokumentácia",
|
||||||
|
"openMainMenu": "Otvoriť hlavné menu",
|
||||||
|
"language": "Jazyk",
|
||||||
|
"searchLanguage": "Hľadať jazyk…",
|
||||||
|
"noLanguagesFound": "Nenašli sa žiadne jazyky"
|
||||||
|
},
|
||||||
|
"donation": {
|
||||||
|
"message": "Páči sa vám BentoPDF? Pomôžte nám udržať ho bezplatný a open source!",
|
||||||
|
"button": "Prispieť"
|
||||||
|
},
|
||||||
|
"hero": {
|
||||||
|
"title": "Sada",
|
||||||
|
"pdfToolkit": "PDF nástrojov",
|
||||||
|
"builtForPrivacy": "vytvorená pre súkromie",
|
||||||
|
"noSignups": "Žiadne registrácie",
|
||||||
|
"unlimitedUse": "Neobmedzené používanie",
|
||||||
|
"worksOffline": "Funguje offline",
|
||||||
|
"startUsing": "Začať používať teraz"
|
||||||
|
},
|
||||||
|
"usedBy": {
|
||||||
|
"title": "Používajú spoločnosti a ľudia, ktorí pracujú v"
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"title": "Prečo si vybrať",
|
||||||
|
"bentoPdf": "BentoPDF?",
|
||||||
|
"noSignup": {
|
||||||
|
"title": "Žiadna registrácia",
|
||||||
|
"description": "Začnite okamžite, nepotrebujete žiadne účty ani e-maily."
|
||||||
|
},
|
||||||
|
"noUploads": {
|
||||||
|
"title": "Žiadne nahrávanie na server",
|
||||||
|
"description": "100% lokálne spracovanie, vaše súbory nikdy neopustia vaše zariadenie."
|
||||||
|
},
|
||||||
|
"foreverFree": {
|
||||||
|
"title": "Navždy zadarmo",
|
||||||
|
"description": "Všetky nástroje, žiadne skúšobné verzie, žiadne skryté poplatky."
|
||||||
|
},
|
||||||
|
"noLimits": {
|
||||||
|
"title": "Bez obmedzení",
|
||||||
|
"description": "Používajte koľko chcete, bez skrytých limitov."
|
||||||
|
},
|
||||||
|
"batchProcessing": {
|
||||||
|
"title": "Dávkové spracovanie",
|
||||||
|
"description": "Spracujte neobmedzené množstvo PDF dokumentov naraz."
|
||||||
|
},
|
||||||
|
"lightningFast": {
|
||||||
|
"title": "Bleskovo rýchle",
|
||||||
|
"description": "Spracujte PDF okamžite, bez zbytočného čakania."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"title": "Začnite s",
|
||||||
|
"toolsLabel": "Nástroje",
|
||||||
|
"subtitle": "Kliknutím na nástroj otvoríte nahrávanie súborov",
|
||||||
|
"searchPlaceholder": "Vyhľadajte nástroj (napr. 'rozdeliť', 'usporiadať'...)",
|
||||||
|
"backToTools": "Späť na nástroje",
|
||||||
|
"firstLoadNotice": "Prvé načítanie chvíľu trvá, kým sa stiahne náš konverzný nástroj. Potom už budú všetky procesy okamžité."
|
||||||
|
},
|
||||||
|
"upload": {
|
||||||
|
"clickToSelect": "Kliknite pre výber súboru",
|
||||||
|
"orDragAndDrop": "alebo ho potiahnite a pusťte",
|
||||||
|
"pdfOrImages": "PDF alebo obrázky",
|
||||||
|
"filesNeverLeave": "Vaše súbory nikdy neopustia vaše zariadenie.",
|
||||||
|
"addMore": "Pridať ďalšie súbory",
|
||||||
|
"clearAll": "Vymazať všetko",
|
||||||
|
"clearFiles": "Vymazať súbory",
|
||||||
|
"hints": {
|
||||||
|
"singlePdf": "Jeden PDF súbor",
|
||||||
|
"pdfFile": "PDF súbor",
|
||||||
|
"multiplePdfs2": "Viacero PDF súborov (aspoň 2)",
|
||||||
|
"bmpImages": "Obrázky BMP",
|
||||||
|
"oneOrMorePdfs": "Jeden alebo viac PDF súborov",
|
||||||
|
"pdfDocuments": "PDF dokumenty",
|
||||||
|
"oneOrMoreCsv": "Jeden alebo viac CSV súborov",
|
||||||
|
"multiplePdfsSupported": "Podporuje sa viacero PDF súborov",
|
||||||
|
"singleOrMultiplePdfs": "Podporovaný jeden alebo viacero PDF súborov",
|
||||||
|
"singlePdfFile": "Jeden PDF súbor",
|
||||||
|
"pdfWithForms": "PDF súbor s poľami formulára",
|
||||||
|
"heicImages": "Obrázky HEIC/HEIF",
|
||||||
|
"jpgImages": "Obrázky JPG, JPEG, JP2, JPX",
|
||||||
|
"pdfsOrImages": "PDF alebo obrázky",
|
||||||
|
"oneOrMoreOdt": "Jeden alebo viac ODT súborov",
|
||||||
|
"singlePdfOnly": "Iba jeden PDF súbor",
|
||||||
|
"pdfFiles": "PDF súbory",
|
||||||
|
"multiplePdfs": "Viacero PDF súborov",
|
||||||
|
"pngImages": "Obrázky PNG",
|
||||||
|
"pdfFilesOneOrMore": "PDF súbory (jeden alebo viac)",
|
||||||
|
"oneOrMoreRtf": "Jeden alebo viac RTF súborov",
|
||||||
|
"svgGraphics": "Grafika SVG",
|
||||||
|
"tiffImages": "Obrázky TIFF",
|
||||||
|
"webpImages": "Obrázky WebP"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"howItWorks": {
|
||||||
|
"title": "Ako to funguje",
|
||||||
|
"step1Title": "Nahrať súbor",
|
||||||
|
"step1": "Kliknite alebo presuňte súbor pre začiatok",
|
||||||
|
"step2Title": "Spracovať",
|
||||||
|
"step2": "Kliknutím na tlačidlo spracovať začnete",
|
||||||
|
"step3Title": "Stiahnuť",
|
||||||
|
"step3": "Okamžite si uložte svoj spracovaný súbor"
|
||||||
|
},
|
||||||
|
"relatedTools": {
|
||||||
|
"title": "Súvisiace PDF nástroje"
|
||||||
|
},
|
||||||
|
"loader": {
|
||||||
|
"processing": "Spracováva sa..."
|
||||||
|
},
|
||||||
|
"alert": {
|
||||||
|
"title": "Upozornenie",
|
||||||
|
"ok": "OK"
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"title": "Náhľad dokumentu",
|
||||||
|
"downloadAsPdf": "Stiahnuť ako PDF",
|
||||||
|
"close": "Zavrieť"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Nastavenia",
|
||||||
|
"shortcuts": "Klávesové skratky",
|
||||||
|
"preferences": "Predvoľby",
|
||||||
|
"displayPreferences": "Predvoľby zobrazenia",
|
||||||
|
"searchShortcuts": "Hľadať skratky...",
|
||||||
|
"shortcutsInfo": "Stlačením a podržaním klávesov nastavíte skratku. Zmeny sa automaticky ukladajú.",
|
||||||
|
"shortcutsWarning": "⚠️ Vyhnite sa bežným skratkám prehliadača (Cmd/Ctrl+W, Cmd/Ctrl+T, Cmd/Ctrl+N atď.), pretože nemusia fungovať spoľahlivo.",
|
||||||
|
"import": "Importovať",
|
||||||
|
"export": "Exportovať",
|
||||||
|
"resetToDefaults": "Obnoviť predvolené",
|
||||||
|
"fullWidthMode": "Režim celej šírky",
|
||||||
|
"fullWidthDescription": "Použiť celú šírku obrazovky pre všetky nástroje namiesto vycentrovaného rozloženia",
|
||||||
|
"compactMode": "Kompaktný režim",
|
||||||
|
"compactModeDescription": "Zobraziť nástroje ako kompaktný zoznam namiesto kariet",
|
||||||
|
"advancedSettings": "Pokročilé nastavenia",
|
||||||
|
"advancedSettingsDescription": "Konfigurácia externých modulov pre spracovanie (PyMuPDF, Ghostscript, CoherentPDF)",
|
||||||
|
"settingsAutoSaved": "Nastavenia sa automaticky ukladajú",
|
||||||
|
"clickToSet": "Kliknutím nastavte",
|
||||||
|
"pressKeys": "Stlačte klávesy...",
|
||||||
|
"warnings": {
|
||||||
|
"alreadyInUse": "Skratka sa už používa",
|
||||||
|
"assignedTo": "je už priradená k:",
|
||||||
|
"chooseDifferent": "Zvoľte si prosím inú skratku.",
|
||||||
|
"reserved": "Upozornenie na vyhradenú skratku",
|
||||||
|
"commonlyUsed": "sa bežne používa pre:",
|
||||||
|
"unreliable": "Táto skratka nemusí fungovať spoľahlivo alebo môže spôsobovať konflikt s prehliadačom/systémom.",
|
||||||
|
"useAnyway": "Chcete ju napriek tomu použiť?",
|
||||||
|
"resetTitle": "Obnoviť skratky",
|
||||||
|
"resetMessage": "Naozaj chcete obnoviť všetky skratky na predvolené?<br><br>Túto akciu nie je možné vrátiť späť.",
|
||||||
|
"importSuccessTitle": "Import úspešný",
|
||||||
|
"importSuccessMessage": "Skratky boli úspešne importované!",
|
||||||
|
"importFailTitle": "Import zlyhal",
|
||||||
|
"importFailMessage": "Nepodarilo sa importovať skratky. Neplatný formát súboru."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"warning": {
|
||||||
|
"title": "Varovanie",
|
||||||
|
"cancel": "Zrušiť",
|
||||||
|
"proceed": "Pokračovať"
|
||||||
|
},
|
||||||
|
"compliance": {
|
||||||
|
"title": "Vaše dáta nikdy neopustia vaše zariadenie",
|
||||||
|
"weKeep": "Udržujeme",
|
||||||
|
"yourInfoSafe": "vaše informácie v bezpečí",
|
||||||
|
"byFollowingStandards": "dodržiavaním globálnych bezpečnostných štandardov.",
|
||||||
|
"processingLocal": "Všetko spracovanie prebieha lokálne priamo na vašom zariadení.",
|
||||||
|
"gdpr": {
|
||||||
|
"title": "Súlad s GDPR",
|
||||||
|
"description": "Chráni osobné údaje a súkromie jednotlivcov v rámci Európskej únie."
|
||||||
|
},
|
||||||
|
"ccpa": {
|
||||||
|
"title": "Súlad s CCPA",
|
||||||
|
"description": "Dáva obyvateľom Kalifornie práva týkajúce sa toho, ako sa zhromažďujú, používajú a zdieľajú ich osobné informácie."
|
||||||
|
},
|
||||||
|
"hipaa": {
|
||||||
|
"title": "Súlad s HIPAA",
|
||||||
|
"description": "Stanovuje ochranné opatrenia pre nakladanie s citlivými zdravotnými informáciami v systéme zdravotnej starostlivosti v USA."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"faq": {
|
||||||
|
"title": "Často kladené",
|
||||||
|
"questions": "otázky",
|
||||||
|
"sectionTitle": "Často kladené otázky",
|
||||||
|
"isFree": {
|
||||||
|
"question": "Je BentoPDF naozaj zadarmo?",
|
||||||
|
"answer": "Áno, absolútne. Všetky nástroje na BentoPDF sú na 100% bezplatné, bez obmedzení súborov, bez registrácie a bez vodotlače. Veríme, že každý si zaslúži prístup k jednoduchým a výkonným nástrojom PDF bez poplatkov."
|
||||||
|
},
|
||||||
|
"areFilesSecure": {
|
||||||
|
"question": "Sú moje súbory v bezpečí? Kde sa spracúvajú?",
|
||||||
|
"answer": "Vaše súbory sú v maximálnom bezpečí, pretože nikdy neopustia váš počítač. Všetko spracovanie prebieha priamo vo vašom webovom prehliadači (na strane klienta). Nikdy nenahrávame vaše súbory na server, takže máte úplné súkromie a kontrolu nad svojimi dokumentmi."
|
||||||
|
},
|
||||||
|
"platforms": {
|
||||||
|
"question": "Funguje to na Macu, vo Windows a na mobilných zariadeniach?",
|
||||||
|
"answer": "Áno! Keďže BentoPDF beží výlučne vo vašom prehliadači, funguje na akomkoľvek operačnom systéme s moderným webovým prehliadačom vrátane Windows, macOS, Linux, iOS a Android."
|
||||||
|
},
|
||||||
|
"gdprCompliant": {
|
||||||
|
"question": "Je BentoPDF v súlade s GDPR?",
|
||||||
|
"answer": "Áno. BentoPDF je plne v súlade s GDPR. Keďže všetko spracovanie súborov prebieha lokálne vo vašom prehliadači a my vaše súbory nikdy nezhromažďujeme ani neprenášame na žiadny server, nemáme prístup k vašim údajom. To zaisťuje, že máte svoje dokumenty vždy pod kontrolou."
|
||||||
|
},
|
||||||
|
"dataStorage": {
|
||||||
|
"question": "Ukladáte alebo sledujete nejaké moje súbory?",
|
||||||
|
"answer": "Nie. Vaše súbory nikdy neukladáme, nesledujeme ani nezaznamenávame. Všetko, čo v BentoPDF urobíte, sa odohráva v pamäti vášho prehliadača a po zatvorení stránky zmizne. Nedochádza k žiadnemu nahrávaniu, logovaniu ani sa nevyužívajú žiadne servery."
|
||||||
|
},
|
||||||
|
"different": {
|
||||||
|
"question": "V čom je BentoPDF odlišné od iných PDF nástrojov?",
|
||||||
|
"answer": "Väčšina PDF nástrojov nahráva vaše súbory na spracovanie na server. BentoPDF to nerobí. Na spracovanie vašich súborov priamo vo vašom prehliadači používame bezpečné moderné webové technológie. Znamená to vyšší výkon, silnejšiu ochranu súkromia a úplný pokoj v duši."
|
||||||
|
},
|
||||||
|
"browserBased": {
|
||||||
|
"question": "Ako ma spracovanie založené na prehliadači chráni?",
|
||||||
|
"answer": "Tým, že BentoPDF beží výlučne vnútri vášho prehliadača, zabezpečuje, že vaše súbory nikdy neopustia vaše zariadenie. Tým sa eliminujú riziká napadnutia serverov, úniku dát alebo neoprávneného prístupu. Vaše súbory zostávajú vždy vaše."
|
||||||
|
},
|
||||||
|
"analytics": {
|
||||||
|
"question": "Používate súbory cookie alebo analytiku na moje sledovanie?",
|
||||||
|
"answer": "Záleží nám na vašom súkromí. BentoPDF nesleduje osobné údaje. Nástroj Simple Analytics využívame výhradne na zobrazenie anonymného počtu návštev. Znamená to, že môžeme vedieť, koľko používateľov navštívilo našu stránku, ale nikdy nevieme, kto ste. Simple Analytics je plne v súlade s GDPR a rešpektuje vaše súkromie."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"testimonials": {
|
||||||
|
"title": "Čo hovoria",
|
||||||
|
"users": "naši",
|
||||||
|
"say": "používatelia"
|
||||||
|
},
|
||||||
|
"support": {
|
||||||
|
"title": "Páči sa vám moja práca?",
|
||||||
|
"description": "BentoPDF je srdcový projekt, vytvorený s cieľom poskytnúť bezplatnú, súkromnú a výkonnú sadu PDF nástrojov pre každého. Ak sa vám zdá užitočný, zvážte podporu jeho vývoja. Každá káva pomôže!",
|
||||||
|
"buyMeCoffee": "Kúpte mi kávu"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"copyright": "© 2026 BentoPDF. Všetky práva vyhradené.",
|
||||||
|
"version": "Verzia",
|
||||||
|
"company": "Spoločnosť",
|
||||||
|
"aboutUs": "O nás",
|
||||||
|
"faqLink": "Časté otázky",
|
||||||
|
"contactUs": "Kontaktujte nás",
|
||||||
|
"legal": "Právne informácie",
|
||||||
|
"termsAndConditions": "Obchodné podmienky",
|
||||||
|
"privacyPolicy": "Zásady ochrany osobných údajov",
|
||||||
|
"followUs": "Sledujte nás"
|
||||||
|
},
|
||||||
|
"merge": {
|
||||||
|
"title": "Zlúčiť PDF",
|
||||||
|
"description": "Spojte celé súbory alebo vyberte konkrétne strany na zlúčenie do nového dokumentu.",
|
||||||
|
"fileMode": "Režim súborov",
|
||||||
|
"pageMode": "Režim strán",
|
||||||
|
"howItWorks": "Ako to funguje:",
|
||||||
|
"fileModeInstructions": [
|
||||||
|
"Kliknite a potiahnite ikonu pre zmenu poradia súborov.",
|
||||||
|
"V poli \"Strany\" pri každom súbore môžete zadať rozsahy (napr. \"1-3, 5\") na zlúčenie iba týchto strán.",
|
||||||
|
"Ak chcete zahrnúť všetky strany z daného súboru, nechajte pole \"Strany\" prázdne."
|
||||||
|
],
|
||||||
|
"pageModeInstructions": [
|
||||||
|
"Všetky strany z vašich nahratých PDF dokumentov sú zobrazené nižšie.",
|
||||||
|
"Jednoducho potiahnite a pusťte jednotlivé miniatúry strán a vytvorte presné poradie pre váš nový súbor."
|
||||||
|
],
|
||||||
|
"mergePdfs": "Zlúčiť PDF"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"page": "Strana",
|
||||||
|
"pages": "Strany",
|
||||||
|
"of": "z",
|
||||||
|
"download": "Stiahnuť",
|
||||||
|
"cancel": "Zrušiť",
|
||||||
|
"save": "Uložiť",
|
||||||
|
"delete": "Zmazať",
|
||||||
|
"edit": "Upraviť",
|
||||||
|
"add": "Pridať",
|
||||||
|
"remove": "Odstrániť",
|
||||||
|
"loading": "Načítava sa...",
|
||||||
|
"loadingPageCount": "Načítavajú sa strany...",
|
||||||
|
"error": "Chyba",
|
||||||
|
"success": "Úspech",
|
||||||
|
"file": "Súbor",
|
||||||
|
"files": "Súbory",
|
||||||
|
"close": "Zavrieť",
|
||||||
|
"convert": "Konvertovať",
|
||||||
|
"unknownError": "Neznáma chyba"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"hero": {
|
||||||
|
"title": "Veríme, že PDF nástroje by mali byť",
|
||||||
|
"subtitle": "rýchle, súkromné a bezplatné.",
|
||||||
|
"noCompromises": "Bez kompromisov."
|
||||||
|
},
|
||||||
|
"mission": {
|
||||||
|
"title": "Naša misia",
|
||||||
|
"description": "Poskytnúť najkomplexnejšiu sadu PDF nástrojov, ktorá rešpektuje vaše súkromie a nikdy nepožaduje platbu. Veríme, že základné nástroje pre prácu s dokumentmi by mali byť dostupné pre každého, všade a bez bariér."
|
||||||
|
},
|
||||||
|
"philosophy": {
|
||||||
|
"label": "Naša hlavná filozofia",
|
||||||
|
"title": "Súkromie na prvom mieste. Vždy.",
|
||||||
|
"description": "V dobe, kedy sú dáta komoditou, k tomu pristupujeme inak. Všetko spracovanie nástrojov BentoPDF prebieha lokálne vo vašom prehliadači. To znamená, že vaše súbory sa nikdy nedotknú našich serverov, nikdy nevidíme vaše dokumenty a nesledujeme, čo robíte. Vaše dokumenty zostávajú úplne a jednoznačne súkromné. Nie je to len funkcia, je to náš základ."
|
||||||
|
},
|
||||||
|
"whyBentopdf": {
|
||||||
|
"title": "Prečo",
|
||||||
|
"speed": {
|
||||||
|
"title": "Zostavené pre rýchlosť",
|
||||||
|
"description": "Žiadne čakanie na nahrávanie alebo sťahovanie zo servera. Spracovaním súborov priamo vo vašom prehliadači pomocou moderných webových technológií ako WebAssembly, ponúkame bezkonkurenčnú rýchlosť pre všetky naše nástroje."
|
||||||
|
},
|
||||||
|
"free": {
|
||||||
|
"title": "Úplne zadarmo",
|
||||||
|
"description": "Žiadne skúšobné verzie, žiadne predplatné, žiadne skryté poplatky a žiadne „prémiové“ funkcie držané ako rukojemníci. Veríme, že výkonné PDF nástroje by mali byť verejnou službou, nie centrom zisku."
|
||||||
|
},
|
||||||
|
"noAccount": {
|
||||||
|
"title": "Nevyžaduje sa žiadny účet",
|
||||||
|
"description": "Začnite používať akýkoľvek nástroj okamžite. Nepotrebujeme váš e-mail, heslo ani žiadne osobné údaje. Váš pracovný postup by mal byť plynulý a anonymný."
|
||||||
|
},
|
||||||
|
"openSource": {
|
||||||
|
"title": "Duch open-source",
|
||||||
|
"description": "Vytvorené s ohľadom na transparentnosť. Využívame neuveriteľné open-source knižnice ako PDF-lib a PDF.js, a veríme v úsilie poháňané komunitou s cieľom sprístupniť výkonné nástroje všetkým."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cta": {
|
||||||
|
"title": "Ste pripravení začať?",
|
||||||
|
"description": "Pridajte sa k tisícom používateľov, ktorí dôverujú BentoPDF vo svojich každodenných potrebách s dokumentmi. Vyskúšajte, aký rozdiel môže urobiť súkromie a výkon.",
|
||||||
|
"button": "Preskúmať všetky nástroje"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"title": "Kontaktujte nás",
|
||||||
|
"subtitle": "Radi o vás budeme počuť. Či už máte otázku, spätnú väzbu, alebo návrh na novú funkciu, neváhajte sa ozvať.",
|
||||||
|
"email": "Môžete nás kontaktovať priamo e-mailom na:"
|
||||||
|
},
|
||||||
|
"licensing": {
|
||||||
|
"title": "Licencovanie pre",
|
||||||
|
"subtitle": "Vyberte si licenciu, ktorá vyhovuje vašim potrebám."
|
||||||
|
},
|
||||||
|
"multiTool": {
|
||||||
|
"uploadPdfs": "Nahrať PDF",
|
||||||
|
"upload": "Nahrať",
|
||||||
|
"addBlankPage": "Pridať prázdnu stranu",
|
||||||
|
"edit": "Upraviť:",
|
||||||
|
"undo": "Späť",
|
||||||
|
"redo": "Znova",
|
||||||
|
"reset": "Obnoviť",
|
||||||
|
"selection": "Výber:",
|
||||||
|
"selectAll": "Vybrať všetko",
|
||||||
|
"deselectAll": "Zrušiť výber",
|
||||||
|
"rotate": "Otočiť:",
|
||||||
|
"rotateLeft": "Doľava",
|
||||||
|
"rotateRight": "Doprava",
|
||||||
|
"transform": "Transformovať:",
|
||||||
|
"duplicate": "Duplikovať",
|
||||||
|
"split": "Rozdeliť",
|
||||||
|
"clear": "Vyčistiť:",
|
||||||
|
"delete": "Zmazať",
|
||||||
|
"download": "Stiahnuť:",
|
||||||
|
"downloadSelected": "Stiahnuť vybrané",
|
||||||
|
"exportPdf": "Exportovať PDF",
|
||||||
|
"uploadPdfFiles": "Vyberte PDF alebo obrázky",
|
||||||
|
"dragAndDrop": "Potiahnite a pusťte PDF alebo obrázky sem, alebo kliknite pre výber",
|
||||||
|
"selectFiles": "Vybrať súbory",
|
||||||
|
"dropToAdd": "Pusťte PDF alebo obrázky pre pridanie",
|
||||||
|
"convertingImages": "Konvertujú sa obrázky do PDF...",
|
||||||
|
"failedToConvertImages": "Nepodarilo sa konvertovať obrázky do PDF.",
|
||||||
|
"renderingPages": "Vykresľujú sa strany...",
|
||||||
|
"actions": {
|
||||||
|
"duplicatePage": "Duplikovať túto stranu",
|
||||||
|
"deletePage": "Zmazať túto stranu",
|
||||||
|
"insertPdf": "Vložiť PDF za túto stranu",
|
||||||
|
"toggleSplit": "Prepnúť rozdelenie za touto stranou"
|
||||||
|
},
|
||||||
|
"pleaseWait": "Prosím čakajte",
|
||||||
|
"pagesRendering": "Strany sa stále vykresľujú. Prosím čakajte...",
|
||||||
|
"noPagesSelected": "Žiadne vybrané strany",
|
||||||
|
"selectOnePage": "Prosím vyberte aspoň jednu stranu na stiahnutie.",
|
||||||
|
"noPages": "Žiadne strany",
|
||||||
|
"noPagesToExport": "Nie sú tu žiadne strany na export.",
|
||||||
|
"renderingTitle": "Vykresľujú sa náhľady strán",
|
||||||
|
"errorRendering": "Nepodarilo sa vykresliť miniatúry strán",
|
||||||
|
"error": "Chyba",
|
||||||
|
"failedToLoad": "Nepodarilo sa načítať"
|
||||||
|
},
|
||||||
|
"simpleMode": {
|
||||||
|
"title": "Nástroje PDF",
|
||||||
|
"subtitle": "Vyberte si nástroj pre začiatok"
|
||||||
|
},
|
||||||
|
"disabledTool": {
|
||||||
|
"title": "Nástroj nedostupný",
|
||||||
|
"heading": "Tento nástroj bol zakázaný",
|
||||||
|
"message": "Tento nástroj nie je vo vašom nasadení dostupný. Pre viac informácií kontaktujte svojho administrátora.",
|
||||||
|
"backHome": "Späť domov"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -350,9 +350,12 @@
|
|||||||
"download": "Ladda ner:",
|
"download": "Ladda ner:",
|
||||||
"downloadSelected": "Ladda ner markerade",
|
"downloadSelected": "Ladda ner markerade",
|
||||||
"exportPdf": "Exportera PDF",
|
"exportPdf": "Exportera PDF",
|
||||||
"uploadPdfFiles": "Välj PDF-filer",
|
"uploadPdfFiles": "Välj PDF- eller bildfiler",
|
||||||
"dragAndDrop": "Dra och släpp PDF-filer här, eller klicka för att välja",
|
"dragAndDrop": "Dra och släpp PDF- eller bildfiler här, eller klicka för att välja",
|
||||||
"selectFiles": "Välj filer",
|
"selectFiles": "Välj filer",
|
||||||
|
"dropToAdd": "Släpp PDF- eller bildfiler för att lägga till",
|
||||||
|
"convertingImages": "Konverterar bilder till PDF...",
|
||||||
|
"failedToConvertImages": "Kunde inte konvertera bilder till PDF.",
|
||||||
"renderingPages": "Renderar sidor...",
|
"renderingPages": "Renderar sidor...",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "Duplicera denna sida",
|
"duplicatePage": "Duplicera denna sida",
|
||||||
|
|||||||
@@ -350,9 +350,12 @@
|
|||||||
"download": "İndir:",
|
"download": "İndir:",
|
||||||
"downloadSelected": "Seçilenleri İndir",
|
"downloadSelected": "Seçilenleri İndir",
|
||||||
"exportPdf": "PDF Olarak Dışa Aktar",
|
"exportPdf": "PDF Olarak Dışa Aktar",
|
||||||
"uploadPdfFiles": "PDF Dosyalarını Seçin",
|
"uploadPdfFiles": "PDF veya Görsel Dosyalarını Seçin",
|
||||||
"dragAndDrop": "PDF dosyalarını buraya sürükleyip bırakın veya seçmek için tıklayın",
|
"dragAndDrop": "PDF veya görsel dosyalarını buraya sürükleyip bırakın veya seçmek için tıklayın",
|
||||||
"selectFiles": "Dosya Seç",
|
"selectFiles": "Dosya Seç",
|
||||||
|
"dropToAdd": "Eklemek için PDF veya görsel dosyalarını bırakın",
|
||||||
|
"convertingImages": "Görseller PDF'e dönüştürülüyor...",
|
||||||
|
"failedToConvertImages": "Görseller PDF'e dönüştürülemedi.",
|
||||||
"renderingPages": "Sayfalar oluşturuluyor...",
|
"renderingPages": "Sayfalar oluşturuluyor...",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "Bu sayfayı çoğalt",
|
"duplicatePage": "Bu sayfayı çoğalt",
|
||||||
|
|||||||
@@ -350,9 +350,12 @@
|
|||||||
"download": "Завантажити:",
|
"download": "Завантажити:",
|
||||||
"downloadSelected": "Завантажити вибране",
|
"downloadSelected": "Завантажити вибране",
|
||||||
"exportPdf": "Експортувати PDF",
|
"exportPdf": "Експортувати PDF",
|
||||||
"uploadPdfFiles": "Виберіть PDF-файли",
|
"uploadPdfFiles": "Виберіть PDF-файли або зображення",
|
||||||
"dragAndDrop": "Перетягніть PDF або натисніть для вибору",
|
"dragAndDrop": "Перетягніть PDF або зображення сюди, або натисніть для вибору",
|
||||||
"selectFiles": "Вибрати файли",
|
"selectFiles": "Вибрати файли",
|
||||||
|
"dropToAdd": "Відпустіть PDF або зображення для додавання",
|
||||||
|
"convertingImages": "Перетворення зображень на PDF...",
|
||||||
|
"failedToConvertImages": "Не вдалося перетворити зображення на PDF.",
|
||||||
"renderingPages": "Рендеринг сторінок...",
|
"renderingPages": "Рендеринг сторінок...",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "Дублювати сторінку",
|
"duplicatePage": "Дублювати сторінку",
|
||||||
|
|||||||
@@ -350,9 +350,12 @@
|
|||||||
"download": "Tải xuống:",
|
"download": "Tải xuống:",
|
||||||
"downloadSelected": "Tải xuống đã chọn",
|
"downloadSelected": "Tải xuống đã chọn",
|
||||||
"exportPdf": "Xuất PDF",
|
"exportPdf": "Xuất PDF",
|
||||||
"uploadPdfFiles": "Chọn tệp PDF",
|
"uploadPdfFiles": "Chọn tệp PDF hoặc hình ảnh",
|
||||||
"dragAndDrop": "Kéo và thả tệp PDF vào đây, hoặc nhấp để chọn",
|
"dragAndDrop": "Kéo và thả tệp PDF hoặc hình ảnh vào đây, hoặc nhấp để chọn",
|
||||||
"selectFiles": "Chọn tệp",
|
"selectFiles": "Chọn tệp",
|
||||||
|
"dropToAdd": "Thả tệp PDF hoặc hình ảnh để thêm",
|
||||||
|
"convertingImages": "Đang chuyển đổi hình ảnh sang PDF...",
|
||||||
|
"failedToConvertImages": "Không thể chuyển đổi hình ảnh sang PDF.",
|
||||||
"renderingPages": "Đang kết xuất trang...",
|
"renderingPages": "Đang kết xuất trang...",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "Nhân bản trang này",
|
"duplicatePage": "Nhân bản trang này",
|
||||||
|
|||||||
@@ -350,9 +350,12 @@
|
|||||||
"download": "下載:",
|
"download": "下載:",
|
||||||
"downloadSelected": "下載選取的項目",
|
"downloadSelected": "下載選取的項目",
|
||||||
"exportPdf": "匯出 PDF",
|
"exportPdf": "匯出 PDF",
|
||||||
"uploadPdfFiles": "選擇 PDF 檔案",
|
"uploadPdfFiles": "選擇 PDF 或圖片檔案",
|
||||||
"dragAndDrop": "拖放 PDF 檔案至此處,或是點擊以選取",
|
"dragAndDrop": "拖放 PDF 或圖片檔案至此處,或是點擊以選取",
|
||||||
"selectFiles": "選擇檔案",
|
"selectFiles": "選擇檔案",
|
||||||
|
"dropToAdd": "拖放 PDF 或圖片檔案以新增",
|
||||||
|
"convertingImages": "正在將圖片轉換為 PDF...",
|
||||||
|
"failedToConvertImages": "圖片轉換為 PDF 失敗。",
|
||||||
"renderingPages": "渲染頁面...",
|
"renderingPages": "渲染頁面...",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "複製此頁",
|
"duplicatePage": "複製此頁",
|
||||||
|
|||||||
@@ -350,9 +350,12 @@
|
|||||||
"download": "下载:",
|
"download": "下载:",
|
||||||
"downloadSelected": "下载选中",
|
"downloadSelected": "下载选中",
|
||||||
"exportPdf": "导出 PDF",
|
"exportPdf": "导出 PDF",
|
||||||
"uploadPdfFiles": "选择 PDF 文件",
|
"uploadPdfFiles": "选择 PDF 或图片文件",
|
||||||
"dragAndDrop": "将 PDF 文件拖放到此处,或点击选择",
|
"dragAndDrop": "将 PDF 或图片文件拖放到此处,或点击选择",
|
||||||
"selectFiles": "选择文件",
|
"selectFiles": "选择文件",
|
||||||
|
"dropToAdd": "拖放 PDF 或图片文件以添加",
|
||||||
|
"convertingImages": "正在将图片转换为 PDF...",
|
||||||
|
"failedToConvertImages": "图片转换为 PDF 失败。",
|
||||||
"renderingPages": "正在渲染页面...",
|
"renderingPages": "正在渲染页面...",
|
||||||
"actions": {
|
"actions": {
|
||||||
"duplicatePage": "复制此页",
|
"duplicatePage": "复制此页",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const DEFAULT_WASM_ORIGINS = {
|
|||||||
};
|
};
|
||||||
const DEFAULT_CORS_PROXY_ORIGIN =
|
const DEFAULT_CORS_PROXY_ORIGIN =
|
||||||
'https://bentopdf-cors-proxy.bentopdf.workers.dev';
|
'https://bentopdf-cors-proxy.bentopdf.workers.dev';
|
||||||
|
const DEFAULT_OCR_FONT_CDN_ORIGIN = 'https://rawcdn.githack.com';
|
||||||
|
|
||||||
const wasmOrigins = [
|
const wasmOrigins = [
|
||||||
originOf(process.env.VITE_WASM_PYMUPDF_URL) || DEFAULT_WASM_ORIGINS.pymupdf,
|
originOf(process.env.VITE_WASM_PYMUPDF_URL) || DEFAULT_WASM_ORIGINS.pymupdf,
|
||||||
@@ -43,13 +44,15 @@ const tesseractOrigins = uniq([
|
|||||||
const corsProxyOrigin =
|
const corsProxyOrigin =
|
||||||
originOf(process.env.VITE_CORS_PROXY_URL) || DEFAULT_CORS_PROXY_ORIGIN;
|
originOf(process.env.VITE_CORS_PROXY_URL) || DEFAULT_CORS_PROXY_ORIGIN;
|
||||||
|
|
||||||
const ocrFontOrigin = originOf(process.env.VITE_OCR_FONT_BASE_URL);
|
const ocrFontOrigin =
|
||||||
|
originOf(process.env.VITE_OCR_FONT_BASE_URL) || DEFAULT_OCR_FONT_CDN_ORIGIN;
|
||||||
|
|
||||||
const scriptOrigins = uniq([...wasmOrigins, ...tesseractOrigins]);
|
const scriptOrigins = uniq([...wasmOrigins, ...tesseractOrigins]);
|
||||||
const connectOrigins = uniq([
|
const connectOrigins = uniq([
|
||||||
...wasmOrigins,
|
...wasmOrigins,
|
||||||
...tesseractOrigins,
|
...tesseractOrigins,
|
||||||
corsProxyOrigin,
|
corsProxyOrigin,
|
||||||
|
ocrFontOrigin,
|
||||||
]);
|
]);
|
||||||
const fontOrigins = uniq([ocrFontOrigin].filter(Boolean));
|
const fontOrigins = uniq([ocrFontOrigin].filter(Boolean));
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://api.github.com https://fonts.gstatic.com https://cdn.jsdelivr.net https://bentopdf-cors-proxy.bentopdf.workers.dev; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'" always;
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com https://rawcdn.githack.com; connect-src 'self' https://api.github.com https://fonts.gstatic.com https://cdn.jsdelivr.net https://bentopdf-cors-proxy.bentopdf.workers.dev https://rawcdn.githack.com; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'" always;
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
|||||||
@@ -447,6 +447,54 @@
|
|||||||
"created_at": "2026-04-10T12:20:50Z",
|
"created_at": "2026-04-10T12:20:50Z",
|
||||||
"repoId": 1074785178,
|
"repoId": 1074785178,
|
||||||
"pullRequestNo": 650
|
"pullRequestNo": 650
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "marek-mraz",
|
||||||
|
"id": 193615612,
|
||||||
|
"comment_id": 4613282157,
|
||||||
|
"created_at": "2026-06-03T14:14:11Z",
|
||||||
|
"repoId": 1074785178,
|
||||||
|
"pullRequestNo": 736
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mondeep31",
|
||||||
|
"id": 97219431,
|
||||||
|
"comment_id": 4621895746,
|
||||||
|
"created_at": "2026-06-04T11:55:51Z",
|
||||||
|
"repoId": 1074785178,
|
||||||
|
"pullRequestNo": 739
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mdevolde",
|
||||||
|
"id": 73021358,
|
||||||
|
"comment_id": 4623697031,
|
||||||
|
"created_at": "2026-06-04T15:36:43Z",
|
||||||
|
"repoId": 1074785178,
|
||||||
|
"pullRequestNo": 740
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "upmcplanetracker",
|
||||||
|
"id": 219436948,
|
||||||
|
"comment_id": 4660768210,
|
||||||
|
"created_at": "2026-06-09T14:25:39Z",
|
||||||
|
"repoId": 1074785178,
|
||||||
|
"pullRequestNo": 746
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "r0ckarong",
|
||||||
|
"id": 4920542,
|
||||||
|
"comment_id": 4719643792,
|
||||||
|
"created_at": "2026-06-16T14:07:01Z",
|
||||||
|
"repoId": 1074785178,
|
||||||
|
"pullRequestNo": 752
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nine-dev9",
|
||||||
|
"id": 82376723,
|
||||||
|
"comment_id": 4772117353,
|
||||||
|
"created_at": "2026-06-22T19:17:49Z",
|
||||||
|
"repoId": 1074785178,
|
||||||
|
"pullRequestNo": 761
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -160,59 +160,59 @@ export const languageToFontFamily: Record<string, string> = {
|
|||||||
|
|
||||||
export const fontFamilyToUrl: Record<string, string> = {
|
export const fontFamilyToUrl: Record<string, string> = {
|
||||||
'Noto Sans JP':
|
'Noto Sans JP':
|
||||||
'https://raw.githack.com/googlefonts/noto-cjk/main/Sans/OTF/Japanese/NotoSansCJKjp-Regular.otf',
|
'https://rawcdn.githack.com/googlefonts/noto-cjk/f8d157532fbfaeda587e826d4cd5b21a49186f7c/Sans/OTF/Japanese/NotoSansCJKjp-Regular.otf',
|
||||||
'Noto Sans SC':
|
'Noto Sans SC':
|
||||||
'https://raw.githack.com/googlefonts/noto-cjk/main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf',
|
'https://rawcdn.githack.com/googlefonts/noto-cjk/f8d157532fbfaeda587e826d4cd5b21a49186f7c/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf',
|
||||||
'Noto Sans TC':
|
'Noto Sans TC':
|
||||||
'https://raw.githack.com/googlefonts/noto-cjk/main/Sans/OTF/TraditionalChinese/NotoSansCJKtc-Regular.otf',
|
'https://rawcdn.githack.com/googlefonts/noto-cjk/f8d157532fbfaeda587e826d4cd5b21a49186f7c/Sans/OTF/TraditionalChinese/NotoSansCJKtc-Regular.otf',
|
||||||
'Noto Sans KR':
|
'Noto Sans KR':
|
||||||
'https://raw.githack.com/googlefonts/noto-cjk/main/Sans/OTF/Korean/NotoSansCJKkr-Regular.otf',
|
'https://rawcdn.githack.com/googlefonts/noto-cjk/f8d157532fbfaeda587e826d4cd5b21a49186f7c/Sans/OTF/Korean/NotoSansCJKkr-Regular.otf',
|
||||||
'Noto Naskh Arabic':
|
'Noto Naskh Arabic':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoNaskhArabic/NotoNaskhArabic-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoNaskhArabic/NotoNaskhArabic-Regular.ttf',
|
||||||
'Noto Sans Devanagari':
|
'Noto Sans Devanagari':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/unhinted/ttf/NotoSansDevanagari/NotoSansDevanagari-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/unhinted/ttf/NotoSansDevanagari/NotoSansDevanagari-Regular.ttf',
|
||||||
'Noto Sans Bengali':
|
'Noto Sans Bengali':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansBengali/NotoSansBengali-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSansBengali/NotoSansBengali-Regular.ttf',
|
||||||
'Noto Sans Gujarati':
|
'Noto Sans Gujarati':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansGujarati/NotoSansGujarati-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSansGujarati/NotoSansGujarati-Regular.ttf',
|
||||||
'Noto Sans Kannada':
|
'Noto Sans Kannada':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansKannada/NotoSansKannada-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSansKannada/NotoSansKannada-Regular.ttf',
|
||||||
'Noto Sans Malayalam':
|
'Noto Sans Malayalam':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansMalayalam/NotoSansMalayalam-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSansMalayalam/NotoSansMalayalam-Regular.ttf',
|
||||||
'Noto Sans Oriya':
|
'Noto Sans Oriya':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansOriya/NotoSansOriya-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSansOriya/NotoSansOriya-Regular.ttf',
|
||||||
'Noto Sans Gurmukhi':
|
'Noto Sans Gurmukhi':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansGurmukhi/NotoSansGurmukhi-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSansGurmukhi/NotoSansGurmukhi-Regular.ttf',
|
||||||
'Noto Sans Tamil':
|
'Noto Sans Tamil':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansTamil/NotoSansTamil-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSansTamil/NotoSansTamil-Regular.ttf',
|
||||||
'Noto Sans Telugu':
|
'Noto Sans Telugu':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansTelugu/NotoSansTelugu-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSansTelugu/NotoSansTelugu-Regular.ttf',
|
||||||
'Noto Sans Sinhala':
|
'Noto Sans Sinhala':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansSinhala/NotoSansSinhala-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSansSinhala/NotoSansSinhala-Regular.ttf',
|
||||||
'Noto Sans Thai':
|
'Noto Sans Thai':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansThai/NotoSansThai-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSansThai/NotoSansThai-Regular.ttf',
|
||||||
'Noto Sans Khmer':
|
'Noto Sans Khmer':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansKhmer/NotoSansKhmer-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSansKhmer/NotoSansKhmer-Regular.ttf',
|
||||||
'Noto Sans Lao':
|
'Noto Sans Lao':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansLao/NotoSansLao-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSansLao/NotoSansLao-Regular.ttf',
|
||||||
'Noto Sans Myanmar':
|
'Noto Sans Myanmar':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansMyanmar/NotoSansMyanmar-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSansMyanmar/NotoSansMyanmar-Regular.ttf',
|
||||||
'Noto Sans Hebrew':
|
'Noto Sans Hebrew':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansHebrew/NotoSansHebrew-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSansHebrew/NotoSansHebrew-Regular.ttf',
|
||||||
'Noto Sans Georgian':
|
'Noto Sans Georgian':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansGeorgian/NotoSansGeorgian-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSansGeorgian/NotoSansGeorgian-Regular.ttf',
|
||||||
'Noto Sans Ethiopic':
|
'Noto Sans Ethiopic':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansEthiopic/NotoSansEthiopic-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSansEthiopic/NotoSansEthiopic-Regular.ttf',
|
||||||
'Noto Serif Tibetan':
|
'Noto Serif Tibetan':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSerifTibetan/NotoSerifTibetan-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSerifTibetan/NotoSerifTibetan-Regular.ttf',
|
||||||
'Noto Sans Cherokee':
|
'Noto Sans Cherokee':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansCherokee/NotoSansCherokee-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSansCherokee/NotoSansCherokee-Regular.ttf',
|
||||||
'Noto Sans Armenian':
|
'Noto Sans Armenian':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansArmenian/NotoSansArmenian-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSansArmenian/NotoSansArmenian-Regular.ttf',
|
||||||
'Noto Sans Syriac':
|
'Noto Sans Syriac':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSansSyriac/NotoSansSyriac-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSansSyriac/NotoSansSyriac-Regular.ttf',
|
||||||
'Noto Sans':
|
'Noto Sans':
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSans/NotoSans-Regular.ttf',
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSans/NotoSans-Regular.ttf',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getFontUrlForFamily(fontFamily: string): string {
|
export function getFontUrlForFamily(fontFamily: string): string {
|
||||||
|
|||||||
+5
-3
@@ -23,6 +23,7 @@ export const supportedLanguages = [
|
|||||||
'ko',
|
'ko',
|
||||||
'ja',
|
'ja',
|
||||||
'uk',
|
'uk',
|
||||||
|
'sk',
|
||||||
] as const;
|
] as const;
|
||||||
export type SupportedLanguage = (typeof supportedLanguages)[number];
|
export type SupportedLanguage = (typeof supportedLanguages)[number];
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ export const languageNames: Record<SupportedLanguage, string> = {
|
|||||||
ko: '한국어',
|
ko: '한국어',
|
||||||
ja: '日本語',
|
ja: '日本語',
|
||||||
uk: 'Українська',
|
uk: 'Українська',
|
||||||
|
sk: 'Slovenčina',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLanguageFromUrl = (): SupportedLanguage => {
|
export const getLanguageFromUrl = (): SupportedLanguage => {
|
||||||
@@ -62,7 +64,7 @@ export const getLanguageFromUrl = (): SupportedLanguage => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const langMatch = path.match(
|
const langMatch = path.match(
|
||||||
/^\/(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk)(?:\/|$)/
|
/^\/(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk)(?:\/|$)/
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
langMatch &&
|
langMatch &&
|
||||||
@@ -152,7 +154,7 @@ export const changeLanguage = (lang: SupportedLanguage): void => {
|
|||||||
|
|
||||||
let pagePathWithoutLang = relativePath;
|
let pagePathWithoutLang = relativePath;
|
||||||
const langPrefixMatch = relativePath.match(
|
const langPrefixMatch = relativePath.match(
|
||||||
/^\/(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk)(\/.*)?$/
|
/^\/(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk)(\/.*)?$/
|
||||||
);
|
);
|
||||||
if (langPrefixMatch) {
|
if (langPrefixMatch) {
|
||||||
pagePathWithoutLang = langPrefixMatch[2] || '/';
|
pagePathWithoutLang = langPrefixMatch[2] || '/';
|
||||||
@@ -247,7 +249,7 @@ export const rewriteLinks = (): void => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const langPrefixRegex = new RegExp(
|
const langPrefixRegex = new RegExp(
|
||||||
`^(${basePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})?/?(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk)(/|$)`
|
`^(${basePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})?/?(en|ar|fr|es|de|zh|zh-TW|vi|tr|id|it|pt|nl|be|da|ko|sv|ru|ja|uk|sk)(/|$)`
|
||||||
);
|
);
|
||||||
if (langPrefixRegex.test(href)) {
|
if (langPrefixRegex.test(href)) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
+142
-48
@@ -12,6 +12,8 @@ import {
|
|||||||
} from '../utils/render-utils';
|
} from '../utils/render-utils';
|
||||||
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
||||||
import { repairPdfFile } from './repair-pdf.js';
|
import { repairPdfFile } from './repair-pdf.js';
|
||||||
|
import { partitionIncomingFiles } from '../utils/multi-tool-file-input.js';
|
||||||
|
import { convertImagesToPdfFile } from '../utils/images-to-pdf-lib.js';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -101,7 +103,11 @@ function showModal(
|
|||||||
success: 'text-green-400',
|
success: 'text-green-400',
|
||||||
};
|
};
|
||||||
|
|
||||||
modalIcon.innerHTML = `<i data-lucide="${iconMap[type]}" class="w-12 h-12 ${colorMap[type]}"></i>`;
|
modalIcon.replaceChildren();
|
||||||
|
const iconEl = document.createElement('i');
|
||||||
|
iconEl.dataset.lucide = iconMap[type];
|
||||||
|
iconEl.className = `w-12 h-12 ${colorMap[type]}`;
|
||||||
|
modalIcon.appendChild(iconEl);
|
||||||
modal.classList.remove('hidden');
|
modal.classList.remove('hidden');
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
}
|
}
|
||||||
@@ -133,20 +139,25 @@ async function withButtonLoading(
|
|||||||
const button = document.getElementById(buttonId) as HTMLButtonElement;
|
const button = document.getElementById(buttonId) as HTMLButtonElement;
|
||||||
if (!button) return;
|
if (!button) return;
|
||||||
|
|
||||||
const originalContent = button.innerHTML;
|
const originalChildren = Array.from(button.childNodes).map((n) =>
|
||||||
|
n.cloneNode(true)
|
||||||
|
);
|
||||||
const originalPointerEvents = button.style.pointerEvents;
|
const originalPointerEvents = button.style.pointerEvents;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
button.style.pointerEvents = 'none';
|
button.style.pointerEvents = 'none';
|
||||||
button.innerHTML =
|
const spinner = document.createElement('i');
|
||||||
'<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
|
spinner.dataset.lucide = 'loader';
|
||||||
|
spinner.className = 'w-5 h-5 animate-spin text-white';
|
||||||
|
button.replaceChildren(spinner);
|
||||||
|
createIcons({ icons });
|
||||||
|
|
||||||
await action();
|
await action();
|
||||||
} finally {
|
} finally {
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
button.style.pointerEvents = originalPointerEvents;
|
button.style.pointerEvents = originalPointerEvents;
|
||||||
button.innerHTML = originalContent;
|
button.replaceChildren(...originalChildren);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,33 +341,122 @@ function initializeTool() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const uploadArea = document.getElementById('upload-area');
|
setupGlobalDragAndDrop();
|
||||||
if (uploadArea) {
|
|
||||||
uploadArea.addEventListener('dragover', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
uploadArea.classList.add('border-indigo-500');
|
|
||||||
});
|
|
||||||
uploadArea.addEventListener('dragleave', () => {
|
|
||||||
uploadArea.classList.remove('border-indigo-500');
|
|
||||||
});
|
|
||||||
uploadArea.addEventListener('drop', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
uploadArea.classList.remove('border-indigo-500');
|
|
||||||
const files = Array.from(e.dataTransfer?.files || []).filter(
|
|
||||||
(f) => f.type === 'application/pdf'
|
|
||||||
);
|
|
||||||
if (files.length > 0) {
|
|
||||||
loadPdfs(files);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('upload-area')?.classList.remove('hidden');
|
document.getElementById('upload-area')?.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dragOverlay: HTMLDivElement | null = null;
|
||||||
|
let isProcessingDrop = false;
|
||||||
|
|
||||||
|
function showDragOverlay() {
|
||||||
|
if (dragOverlay) return;
|
||||||
|
dragOverlay = document.createElement('div');
|
||||||
|
dragOverlay.className =
|
||||||
|
'fixed inset-0 bg-indigo-900/40 border-4 border-dashed border-indigo-400 z-40 pointer-events-none flex items-center justify-center';
|
||||||
|
const inner = document.createElement('div');
|
||||||
|
inner.className =
|
||||||
|
'text-white text-lg sm:text-xl font-semibold bg-gray-900/80 px-6 py-4 rounded-lg text-center';
|
||||||
|
inner.textContent = t('multiTool.dropToAdd');
|
||||||
|
dragOverlay.appendChild(inner);
|
||||||
|
document.body.appendChild(dragOverlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideDragOverlay() {
|
||||||
|
if (dragOverlay) {
|
||||||
|
dragOverlay.remove();
|
||||||
|
dragOverlay = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupGlobalDragAndDrop() {
|
||||||
|
const target = document.getElementById('main-scroll-container');
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
target.addEventListener('dragover', (e) => {
|
||||||
|
if (!e.dataTransfer?.types.includes('Files')) return;
|
||||||
|
e.preventDefault();
|
||||||
|
showDragOverlay();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('dragleave', (e) => {
|
||||||
|
if (
|
||||||
|
e.clientX <= 0 ||
|
||||||
|
e.clientY <= 0 ||
|
||||||
|
e.clientX >= window.innerWidth ||
|
||||||
|
e.clientY >= window.innerHeight
|
||||||
|
) {
|
||||||
|
hideDragOverlay();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('drop', () => {
|
||||||
|
hideDragOverlay();
|
||||||
|
});
|
||||||
|
|
||||||
|
target.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
hideDragOverlay();
|
||||||
|
const files = Array.from(e.dataTransfer?.files || []);
|
||||||
|
if (files.length === 0) return;
|
||||||
|
void handleIncomingFiles(files);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processIncomingFiles(rawFiles: File[]): Promise<File[]> {
|
||||||
|
const { pdfFiles, imageFiles, skipped } = partitionIncomingFiles(rawFiles);
|
||||||
|
const result: File[] = [...pdfFiles];
|
||||||
|
|
||||||
|
if (imageFiles.length > 0) {
|
||||||
|
try {
|
||||||
|
showLoading(0, 100);
|
||||||
|
const loadingText = document.getElementById('loading-text');
|
||||||
|
if (loadingText)
|
||||||
|
loadingText.textContent = t('multiTool.convertingImages');
|
||||||
|
const imagesPdfFile = await convertImagesToPdfFile(imageFiles);
|
||||||
|
result.push(imagesPdfFile);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to convert images to PDF:', e);
|
||||||
|
hideLoading();
|
||||||
|
showModal(
|
||||||
|
t('multiTool.error'),
|
||||||
|
t('multiTool.failedToConvertImages'),
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipped.length > 0) {
|
||||||
|
console.warn('Skipped unsupported files:', skipped);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleIncomingFiles(rawFiles: File[]) {
|
||||||
|
if (isRendering || isProcessingDrop) {
|
||||||
|
showModal(t('multiTool.pleaseWait'), t('multiTool.pagesRendering'), 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isProcessingDrop = true;
|
||||||
|
try {
|
||||||
|
const processed = await processIncomingFiles(rawFiles);
|
||||||
|
if (processed.length > 0) {
|
||||||
|
await loadPdfs(processed);
|
||||||
|
} else {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isProcessingDrop = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resetAll() {
|
function resetAll() {
|
||||||
renderCancelled = true;
|
renderCancelled = true;
|
||||||
isRendering = false;
|
isRendering = false;
|
||||||
|
isProcessingDrop = false;
|
||||||
|
hideDragOverlay();
|
||||||
snapshot();
|
snapshot();
|
||||||
allPages = [];
|
allPages = [];
|
||||||
selectedPages.clear();
|
selectedPages.clear();
|
||||||
@@ -382,7 +482,7 @@ async function handlePdfUpload(e: Event) {
|
|||||||
const input = e.target as HTMLInputElement;
|
const input = e.target as HTMLInputElement;
|
||||||
const files = Array.from(input.files || []);
|
const files = Array.from(input.files || []);
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
await loadPdfs(files);
|
await handleIncomingFiles(files);
|
||||||
}
|
}
|
||||||
input.value = '';
|
input.value = '';
|
||||||
}
|
}
|
||||||
@@ -524,20 +624,6 @@ async function loadPdfs(files: File[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCacheKey(pdfIndex: number, pageIndex: number): string {
|
|
||||||
return `${pdfIndex}-${pageIndex}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrapper for compatibility with updatePageDisplay
|
|
||||||
function createPageCard(pageData: PageData, index: number) {
|
|
||||||
const pagesContainer = document.getElementById('pages-container');
|
|
||||||
if (!pagesContainer) return;
|
|
||||||
|
|
||||||
const card = createPageElement(pageData.canvas, index);
|
|
||||||
pagesContainer.appendChild(card);
|
|
||||||
createIcons({ icons });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modified to return the element instead of appending it
|
// Modified to return the element instead of appending it
|
||||||
function createPageElement(
|
function createPageElement(
|
||||||
canvas: HTMLCanvasElement | null,
|
canvas: HTMLCanvasElement | null,
|
||||||
@@ -584,10 +670,13 @@ function createPageElement(
|
|||||||
const loading = document.createElement('div');
|
const loading = document.createElement('div');
|
||||||
loading.className =
|
loading.className =
|
||||||
'flex flex-col items-center justify-center text-gray-400';
|
'flex flex-col items-center justify-center text-gray-400';
|
||||||
loading.innerHTML = `
|
const loadingIcon = document.createElement('i');
|
||||||
<i data-lucide="loader" class="w-8 h-8 animate-spin mb-2"></i>
|
loadingIcon.dataset.lucide = 'loader';
|
||||||
<span class="text-xs">${t('common.loading')}</span>
|
loadingIcon.className = 'w-8 h-8 animate-spin mb-2';
|
||||||
`;
|
const loadingLabel = document.createElement('span');
|
||||||
|
loadingLabel.className = 'text-xs';
|
||||||
|
loadingLabel.textContent = t('common.loading');
|
||||||
|
loading.append(loadingIcon, loadingLabel);
|
||||||
preview.appendChild(loading);
|
preview.appendChild(loading);
|
||||||
preview.classList.add('bg-gray-700'); // Darker background for loading
|
preview.classList.add('bg-gray-700'); // Darker background for loading
|
||||||
}
|
}
|
||||||
@@ -611,9 +700,15 @@ function createPageElement(
|
|||||||
const selectBtn = document.createElement('button');
|
const selectBtn = document.createElement('button');
|
||||||
selectBtn.className =
|
selectBtn.className =
|
||||||
'absolute top-2 right-2 p-1 rounded bg-gray-900/70 hover:bg-gray-800 z-10';
|
'absolute top-2 right-2 p-1 rounded bg-gray-900/70 hover:bg-gray-800 z-10';
|
||||||
selectBtn.innerHTML = selectedPages.has(index)
|
const selectIcon = document.createElement('i');
|
||||||
? '<i data-lucide="check-square" class="w-4 h-4 text-indigo-400"></i>'
|
if (selectedPages.has(index)) {
|
||||||
: '<i data-lucide="square" class="w-4 h-4 text-gray-200"></i>';
|
selectIcon.dataset.lucide = 'check-square';
|
||||||
|
selectIcon.className = 'w-4 h-4 text-indigo-400';
|
||||||
|
} else {
|
||||||
|
selectIcon.dataset.lucide = 'square';
|
||||||
|
selectIcon.className = 'w-4 h-4 text-gray-200';
|
||||||
|
}
|
||||||
|
selectBtn.appendChild(selectIcon);
|
||||||
selectBtn.onclick = (e) => {
|
selectBtn.onclick = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleSelectOptimized(index);
|
toggleSelectOptimized(index);
|
||||||
@@ -1094,7 +1189,6 @@ async function downloadAll() {
|
|||||||
async function downloadSplitPdfs() {
|
async function downloadSplitPdfs() {
|
||||||
try {
|
try {
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
const sortedMarkers = Array.from(splitMarkers).sort((a, b) => a - b);
|
|
||||||
|
|
||||||
// Create segments based on split markers
|
// Create segments based on split markers
|
||||||
const segments: number[][] = [];
|
const segments: number[][] = [];
|
||||||
|
|||||||
+115
-142
@@ -2,7 +2,12 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { t } from '../i18n/i18n';
|
import { t } from '../i18n/i18n';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { downloadFile, getPDFDocument, formatBytes } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
getPDFDocument,
|
||||||
|
formatBytes,
|
||||||
|
initializeQpdf,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import {
|
import {
|
||||||
@@ -13,8 +18,18 @@ 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';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
|
||||||
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
import { loadPdfDocument } from '../utils/load-pdf-document.js';
|
||||||
|
import type { QpdfInstanceExtended } from '@/types';
|
||||||
|
import {
|
||||||
|
parseRangeGroups,
|
||||||
|
evenOddIndices,
|
||||||
|
allPagesIndices,
|
||||||
|
nTimesGroups,
|
||||||
|
bookmarkSplitGroups,
|
||||||
|
groupFilename,
|
||||||
|
uniqueZipName,
|
||||||
|
extractPagesWithQpdf,
|
||||||
|
} from '../utils/split-pdf-helpers.js';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -23,6 +38,7 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
|||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
let visualSelectorRendered = false;
|
let visualSelectorRendered = false;
|
||||||
|
let isSplitting = false;
|
||||||
|
|
||||||
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');
|
||||||
@@ -38,7 +54,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const rangePanel = document.getElementById('range-panel');
|
const rangePanel = document.getElementById('range-panel');
|
||||||
const visualPanel = document.getElementById('visual-select-panel');
|
const visualPanel = document.getElementById('visual-select-panel');
|
||||||
const evenOddPanel = document.getElementById('even-odd-panel');
|
const evenOddPanel = document.getElementById('even-odd-panel');
|
||||||
const zipOptionWrapper = document.getElementById('zip-option-wrapper');
|
const outputModeWrapper = document.getElementById('output-mode-wrapper');
|
||||||
|
const outputSeparateLabel = document.getElementById('output-separate-label');
|
||||||
const allPagesPanel = document.getElementById('all-pages-panel');
|
const allPagesPanel = document.getElementById('all-pages-panel');
|
||||||
const bookmarksPanel = document.getElementById('bookmarks-panel');
|
const bookmarksPanel = document.getElementById('bookmarks-panel');
|
||||||
const nTimesPanel = document.getElementById('n-times-panel');
|
const nTimesPanel = document.getElementById('n-times-panel');
|
||||||
@@ -252,6 +269,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
if (nValueInput) nValueInput.value = '5';
|
if (nValueInput) nValueInput.value = '5';
|
||||||
|
|
||||||
|
const combineRadio = document.getElementById(
|
||||||
|
'output-combine'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
if (combineRadio) combineRadio.checked = true;
|
||||||
|
|
||||||
// Reset radio buttons to default (range)
|
// Reset radio buttons to default (range)
|
||||||
const rangeRadio = document.querySelector(
|
const rangeRadio = document.querySelector(
|
||||||
'input[name="split-mode"][value="range"]'
|
'input[name="split-mode"][value="range"]'
|
||||||
@@ -271,18 +293,50 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const split = async () => {
|
const split = async () => {
|
||||||
|
if (isSplitting) return;
|
||||||
|
isSplitting = true;
|
||||||
|
if (processBtn) (processBtn as HTMLButtonElement).disabled = true;
|
||||||
|
|
||||||
const splitMode = splitModeSelect.value;
|
const splitMode = splitModeSelect.value;
|
||||||
const downloadAsZip =
|
const oneFilePerUnit =
|
||||||
(document.getElementById('download-as-zip') as HTMLInputElement)
|
(
|
||||||
?.checked || false;
|
document.querySelector(
|
||||||
|
'input[name="split-output-mode"]:checked'
|
||||||
|
) as HTMLInputElement | null
|
||||||
|
)?.value === 'separate';
|
||||||
|
|
||||||
showLoader('Splitting PDF...');
|
showLoader('Splitting PDF...');
|
||||||
|
|
||||||
|
let qpdf: QpdfInstanceExtended | null = null;
|
||||||
|
const inputPath = '/split-input.pdf';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!state.pdfDoc) throw new Error('No PDF document loaded.');
|
if (!state.pdfDoc) throw new Error('No PDF document loaded.');
|
||||||
|
const srcDoc = state.pdfDoc;
|
||||||
|
|
||||||
const totalPages = state.pdfDoc.getPageCount();
|
const totalPages = srcDoc.getPageCount();
|
||||||
let indicesToExtract: number[] = [];
|
let indicesToExtract: number[] = [];
|
||||||
|
let outputGroups: number[][] | null = null;
|
||||||
|
|
||||||
|
let sourceBytes: Uint8Array | null = null;
|
||||||
|
const getSourceBytes = async (): Promise<Uint8Array> => {
|
||||||
|
if (sourceBytes) return sourceBytes;
|
||||||
|
sourceBytes = new Uint8Array(await srcDoc.save());
|
||||||
|
return sourceBytes;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureQpdf = async (): Promise<QpdfInstanceExtended> => {
|
||||||
|
if (qpdf) return qpdf;
|
||||||
|
const instance = await initializeQpdf();
|
||||||
|
instance.FS.writeFile(inputPath, await getSourceBytes());
|
||||||
|
qpdf = instance;
|
||||||
|
return instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractPages = async (indices: number[]): Promise<Uint8Array> => {
|
||||||
|
const instance = await ensureQpdf();
|
||||||
|
return extractPagesWithQpdf(instance, inputPath, indices);
|
||||||
|
};
|
||||||
|
|
||||||
switch (splitMode) {
|
switch (splitMode) {
|
||||||
case 'range': {
|
case 'range': {
|
||||||
@@ -290,71 +344,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
document.getElementById('page-range') as HTMLInputElement
|
document.getElementById('page-range') as HTMLInputElement
|
||||||
).value;
|
).value;
|
||||||
if (!pageRangeInput) throw new Error('Choose a valid page range.');
|
if (!pageRangeInput) throw new Error('Choose a valid page range.');
|
||||||
const ranges = pageRangeInput.split(',');
|
|
||||||
|
|
||||||
const rangeGroups: number[][] = [];
|
const { groups: rangeGroups, indices: rangeIndices } =
|
||||||
for (const range of ranges) {
|
parseRangeGroups(pageRangeInput, totalPages);
|
||||||
const trimmedRange = range.trim();
|
indicesToExtract.push(...rangeIndices);
|
||||||
if (!trimmedRange) continue;
|
|
||||||
|
|
||||||
const groupIndices: number[] = [];
|
if (oneFilePerUnit) outputGroups = rangeGroups;
|
||||||
if (trimmedRange.includes('-')) {
|
|
||||||
const [start, end] = trimmedRange.split('-').map(Number);
|
|
||||||
if (
|
|
||||||
isNaN(start) ||
|
|
||||||
isNaN(end) ||
|
|
||||||
start < 1 ||
|
|
||||||
end > totalPages ||
|
|
||||||
start > end
|
|
||||||
)
|
|
||||||
continue;
|
|
||||||
for (let i = start; i <= end; i++) groupIndices.push(i - 1);
|
|
||||||
} else {
|
|
||||||
const pageNum = Number(trimmedRange);
|
|
||||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages)
|
|
||||||
continue;
|
|
||||||
groupIndices.push(pageNum - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groupIndices.length > 0) {
|
|
||||||
rangeGroups.push(groupIndices);
|
|
||||||
indicesToExtract.push(...groupIndices);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rangeGroups.length > 1) {
|
|
||||||
showLoader('Creating separate PDFs for each range...');
|
|
||||||
const zip = new JSZip();
|
|
||||||
|
|
||||||
for (let i = 0; i < rangeGroups.length; i++) {
|
|
||||||
const group = rangeGroups[i];
|
|
||||||
const newPdf = await PDFLibDocument.create();
|
|
||||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, group);
|
|
||||||
copiedPages.forEach((page) => newPdf.addPage(page));
|
|
||||||
const pdfBytes = await newPdf.save();
|
|
||||||
|
|
||||||
const minPage = Math.min(...group) + 1;
|
|
||||||
const maxPage = Math.max(...group) + 1;
|
|
||||||
const filename =
|
|
||||||
minPage === maxPage
|
|
||||||
? `page-${minPage}.pdf`
|
|
||||||
: `pages-${minPage}-${maxPage}.pdf`;
|
|
||||||
zip.file(filename, pdfBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
|
||||||
downloadFile(zipBlob, 'split-pages.zip');
|
|
||||||
hideLoader();
|
|
||||||
showAlert(
|
|
||||||
'Success',
|
|
||||||
`PDF split into ${rangeGroups.length} files successfully!`,
|
|
||||||
'success',
|
|
||||||
() => {
|
|
||||||
resetState();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,21 +359,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
if (!choiceElement)
|
if (!choiceElement)
|
||||||
throw new Error('Please select even or odd pages.');
|
throw new Error('Please select even or odd pages.');
|
||||||
const choice = choiceElement.value;
|
const choice = choiceElement.value === 'even' ? 'even' : 'odd';
|
||||||
for (let i = 0; i < totalPages; i++) {
|
indicesToExtract = evenOddIndices(choice, totalPages);
|
||||||
if (choice === 'even' && (i + 1) % 2 === 0)
|
|
||||||
indicesToExtract.push(i);
|
|
||||||
if (choice === 'odd' && (i + 1) % 2 !== 0) indicesToExtract.push(i);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'all':
|
case 'all':
|
||||||
indicesToExtract = Array.from({ length: totalPages }, (_, i) => i);
|
indicesToExtract = allPagesIndices(totalPages);
|
||||||
|
outputGroups = indicesToExtract.map((i) => [i]);
|
||||||
break;
|
break;
|
||||||
case 'visual':
|
case 'visual':
|
||||||
indicesToExtract = Array.from(
|
indicesToExtract = Array.from(
|
||||||
document.querySelectorAll('.page-thumbnail-wrapper.selected')
|
document.querySelectorAll('.page-thumbnail-wrapper.selected')
|
||||||
).map((el) => parseInt((el as HTMLElement).dataset.pageIndex || '0'));
|
).map((el) => parseInt((el as HTMLElement).dataset.pageIndex || '0'));
|
||||||
|
if (oneFilePerUnit)
|
||||||
|
outputGroups = [...new Set(indicesToExtract)].map((i) => [i]);
|
||||||
break;
|
break;
|
||||||
case 'bookmarks': {
|
case 'bookmarks': {
|
||||||
if (!isCpdfAvailable()) {
|
if (!isCpdfAvailable()) {
|
||||||
@@ -388,8 +382,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
const { getCpdf } = await import('../utils/cpdf-helper.js');
|
const { getCpdf } = await import('../utils/cpdf-helper.js');
|
||||||
const cpdf = await getCpdf();
|
const cpdf = await getCpdf();
|
||||||
const pdfBytes = await state.pdfDoc.save();
|
const pdf = cpdf.fromMemory(
|
||||||
const pdf = cpdf.fromMemory(new Uint8Array(pdfBytes), '');
|
new Uint8Array(await getSourceBytes()),
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
cpdf.startGetBookmarkInfo(pdf);
|
cpdf.startGetBookmarkInfo(pdf);
|
||||||
const bookmarkCount = cpdf.numberBookmarks();
|
const bookmarkCount = cpdf.numberBookmarks();
|
||||||
@@ -415,27 +411,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
throw new Error('No bookmarks found at the selected level.');
|
throw new Error('No bookmarks found at the selected level.');
|
||||||
}
|
}
|
||||||
|
|
||||||
splitPages.sort((a, b) => a - b);
|
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
|
const bookmarkGroups = bookmarkSplitGroups(splitPages, totalPages);
|
||||||
|
|
||||||
for (let i = 0; i < splitPages.length; i++) {
|
for (let i = 0; i < bookmarkGroups.length; i++) {
|
||||||
const startPage = i === 0 ? 0 : splitPages[i];
|
const pdfBytes2 = await extractPages(bookmarkGroups[i]);
|
||||||
const endPage =
|
|
||||||
i < splitPages.length - 1
|
|
||||||
? splitPages[i + 1] - 1
|
|
||||||
: totalPages - 1;
|
|
||||||
|
|
||||||
const newPdf = await PDFLibDocument.create();
|
|
||||||
const pageIndices = Array.from(
|
|
||||||
{ length: endPage - startPage + 1 },
|
|
||||||
(_, idx) => startPage + idx
|
|
||||||
);
|
|
||||||
const copiedPages = await newPdf.copyPages(
|
|
||||||
state.pdfDoc,
|
|
||||||
pageIndices
|
|
||||||
);
|
|
||||||
copiedPages.forEach((page) => newPdf.addPage(page));
|
|
||||||
const pdfBytes2 = await newPdf.save();
|
|
||||||
zip.file(`split-${i + 1}.pdf`, pdfBytes2);
|
zip.file(`split-${i + 1}.pdf`, pdfBytes2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,23 +436,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (nValue < 1) throw new Error('N must be at least 1.');
|
if (nValue < 1) throw new Error('N must be at least 1.');
|
||||||
|
|
||||||
const zip2 = new JSZip();
|
const zip2 = new JSZip();
|
||||||
const numSplits = Math.ceil(totalPages / nValue);
|
const chunks = nTimesGroups(nValue, totalPages);
|
||||||
|
|
||||||
for (let i = 0; i < numSplits; i++) {
|
for (let i = 0; i < chunks.length; i++) {
|
||||||
const startPage = i * nValue;
|
const pdfBytes3 = await extractPages(chunks[i]);
|
||||||
const endPage = Math.min(startPage + nValue - 1, totalPages - 1);
|
|
||||||
const pageIndices = Array.from(
|
|
||||||
{ length: endPage - startPage + 1 },
|
|
||||||
(_, idx) => startPage + idx
|
|
||||||
);
|
|
||||||
|
|
||||||
const newPdf = await PDFLibDocument.create();
|
|
||||||
const copiedPages = await newPdf.copyPages(
|
|
||||||
state.pdfDoc,
|
|
||||||
pageIndices
|
|
||||||
);
|
|
||||||
copiedPages.forEach((page) => newPdf.addPage(page));
|
|
||||||
const pdfBytes3 = await newPdf.save();
|
|
||||||
zip2.file(`split-${i + 1}.pdf`, pdfBytes3);
|
zip2.file(`split-${i + 1}.pdf`, pdfBytes3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,31 +462,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
throw new Error('No pages were selected for splitting.');
|
throw new Error('No pages were selected for splitting.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (outputGroups && outputGroups.length > 0) {
|
||||||
splitMode === 'all' ||
|
if (outputGroups.length === 1) {
|
||||||
(['range', 'visual'].includes(splitMode) && downloadAsZip)
|
const pdfBytes = await extractPages(outputGroups[0]);
|
||||||
) {
|
downloadFile(
|
||||||
showLoader('Creating ZIP file...');
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
const zip = new JSZip();
|
groupFilename(outputGroups[0])
|
||||||
for (const index of uniqueIndices) {
|
);
|
||||||
const newPdf = await PDFLibDocument.create();
|
} else {
|
||||||
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [
|
showLoader('Creating ZIP file...');
|
||||||
index as number,
|
const zip = new JSZip();
|
||||||
]);
|
const usedNames = new Map<string, number>();
|
||||||
newPdf.addPage(copiedPage);
|
for (const group of outputGroups) {
|
||||||
const pdfBytes = await newPdf.save();
|
const pdfBytes = await extractPages(group);
|
||||||
zip.file(`page-${index + 1}.pdf`, new Uint8Array(pdfBytes));
|
zip.file(uniqueZipName(groupFilename(group), usedNames), pdfBytes);
|
||||||
|
}
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, 'split-pages.zip');
|
||||||
}
|
}
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
|
||||||
downloadFile(zipBlob, 'split-pages.zip');
|
|
||||||
} else {
|
} else {
|
||||||
const newPdf = await PDFLibDocument.create();
|
const pdfBytes = await extractPages(uniqueIndices);
|
||||||
const copiedPages = await newPdf.copyPages(
|
|
||||||
state.pdfDoc,
|
|
||||||
uniqueIndices as number[]
|
|
||||||
);
|
|
||||||
copiedPages.forEach((page) => newPdf.addPage(page));
|
|
||||||
const pdfBytes = await newPdf.save();
|
|
||||||
downloadFile(
|
downloadFile(
|
||||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
'split-document.pdf'
|
'split-document.pdf'
|
||||||
@@ -542,6 +504,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
: 'Failed to split PDF. Please check your selection.'
|
: 'Failed to split PDF. Please check your selection.'
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
if (qpdf) {
|
||||||
|
try {
|
||||||
|
qpdf.FS.unlink(inputPath);
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.warn('Failed to clean up qpdf input file:', cleanupError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isSplitting = false;
|
||||||
|
if (processBtn) (processBtn as HTMLButtonElement).disabled = false;
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -610,15 +581,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
allPagesPanel?.classList.add('hidden');
|
allPagesPanel?.classList.add('hidden');
|
||||||
bookmarksPanel?.classList.add('hidden');
|
bookmarksPanel?.classList.add('hidden');
|
||||||
nTimesPanel?.classList.add('hidden');
|
nTimesPanel?.classList.add('hidden');
|
||||||
zipOptionWrapper?.classList.add('hidden');
|
outputModeWrapper?.classList.add('hidden');
|
||||||
if (nTimesWarning) nTimesWarning.classList.add('hidden');
|
if (nTimesWarning) nTimesWarning.classList.add('hidden');
|
||||||
|
|
||||||
if (mode === 'range') {
|
if (mode === 'range') {
|
||||||
rangePanel?.classList.remove('hidden');
|
rangePanel?.classList.remove('hidden');
|
||||||
zipOptionWrapper?.classList.remove('hidden');
|
outputModeWrapper?.classList.remove('hidden');
|
||||||
|
if (outputSeparateLabel)
|
||||||
|
outputSeparateLabel.textContent = 'One PDF per range';
|
||||||
} else if (mode === 'visual') {
|
} else if (mode === 'visual') {
|
||||||
visualPanel?.classList.remove('hidden');
|
visualPanel?.classList.remove('hidden');
|
||||||
zipOptionWrapper?.classList.remove('hidden');
|
outputModeWrapper?.classList.remove('hidden');
|
||||||
|
if (outputSeparateLabel)
|
||||||
|
outputSeparateLabel.textContent = 'One PDF per page';
|
||||||
renderVisualSelector();
|
renderVisualSelector();
|
||||||
} else if (mode === 'even-odd') {
|
} else if (mode === 'even-odd') {
|
||||||
evenOddPanel?.classList.remove('hidden');
|
evenOddPanel?.classList.remove('hidden');
|
||||||
@@ -626,10 +601,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
allPagesPanel?.classList.remove('hidden');
|
allPagesPanel?.classList.remove('hidden');
|
||||||
} else if (mode === 'bookmarks') {
|
} else if (mode === 'bookmarks') {
|
||||||
bookmarksPanel?.classList.remove('hidden');
|
bookmarksPanel?.classList.remove('hidden');
|
||||||
zipOptionWrapper?.classList.remove('hidden');
|
|
||||||
} else if (mode === 'n-times') {
|
} else if (mode === 'n-times') {
|
||||||
nTimesPanel?.classList.remove('hidden');
|
nTimesPanel?.classList.remove('hidden');
|
||||||
zipOptionWrapper?.classList.remove('hidden');
|
|
||||||
|
|
||||||
const updateWarning = () => {
|
const updateWarning = () => {
|
||||||
if (!state.pdfDoc) return;
|
if (!state.pdfDoc) return;
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
import { preprocessImageFile } from './image-input-utils.js';
|
||||||
|
|
||||||
|
export interface EmbeddableImage {
|
||||||
|
bytes: Uint8Array;
|
||||||
|
format: 'jpg' | 'png';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function normalizeImageToEmbeddable(
|
||||||
|
file: File
|
||||||
|
): Promise<EmbeddableImage> {
|
||||||
|
const processed = await preprocessImageFile(file);
|
||||||
|
const name = processed.name.toLowerCase();
|
||||||
|
const isJpg =
|
||||||
|
processed.type === 'image/jpeg' ||
|
||||||
|
name.endsWith('.jpg') ||
|
||||||
|
name.endsWith('.jpeg');
|
||||||
|
const isPng = processed.type === 'image/png' || name.endsWith('.png');
|
||||||
|
|
||||||
|
if (isJpg) {
|
||||||
|
return {
|
||||||
|
bytes: new Uint8Array(await processed.arrayBuffer()),
|
||||||
|
format: 'jpg',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isPng) {
|
||||||
|
return {
|
||||||
|
bytes: new Uint8Array(await processed.arrayBuffer()),
|
||||||
|
format: 'png',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(processed);
|
||||||
|
try {
|
||||||
|
const img = await new Promise<HTMLImageElement>((resolve, reject) => {
|
||||||
|
const i = new Image();
|
||||||
|
i.onload = () => resolve(i);
|
||||||
|
i.onerror = () =>
|
||||||
|
reject(new Error(`Failed to decode image: ${processed.name}`));
|
||||||
|
i.src = url;
|
||||||
|
});
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = img.naturalWidth;
|
||||||
|
canvas.height = img.naturalHeight;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) throw new Error('Canvas context unavailable');
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
const blob = await new Promise<Blob>((resolve, reject) => {
|
||||||
|
canvas.toBlob(
|
||||||
|
(b) => (b ? resolve(b) : reject(new Error('Canvas toBlob failed'))),
|
||||||
|
'image/png'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return { bytes: new Uint8Array(await blob.arrayBuffer()), format: 'png' };
|
||||||
|
} finally {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function convertImagesToPdfFile(images: File[]): Promise<File> {
|
||||||
|
const pdfDoc = await PDFLibDocument.create();
|
||||||
|
for (const img of images) {
|
||||||
|
const { bytes, format } = await normalizeImageToEmbeddable(img);
|
||||||
|
const embedded =
|
||||||
|
format === 'jpg'
|
||||||
|
? await pdfDoc.embedJpg(bytes)
|
||||||
|
: await pdfDoc.embedPng(bytes);
|
||||||
|
const page = pdfDoc.addPage([embedded.width, embedded.height]);
|
||||||
|
page.drawImage(embedded, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: embedded.width,
|
||||||
|
height: embedded.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
const filename =
|
||||||
|
images.length === 1
|
||||||
|
? images[0].name.replace(/\.[^.]+$/, '.pdf')
|
||||||
|
: `images-${Date.now()}.pdf`;
|
||||||
|
return new File([new Uint8Array(pdfBytes)], filename, {
|
||||||
|
type: 'application/pdf',
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -299,6 +299,7 @@ export class MarkdownEditor {
|
|||||||
startOnLoad: false,
|
startOnLoad: false,
|
||||||
theme: 'default',
|
theme: 'default',
|
||||||
securityLevel: 'strict',
|
securityLevel: 'strict',
|
||||||
|
htmlLabels: false,
|
||||||
fontFamily:
|
fontFamily:
|
||||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||||
});
|
});
|
||||||
@@ -717,9 +718,10 @@ export class MarkdownEditor {
|
|||||||
|
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'mermaid-diagram';
|
wrapper.className = 'mermaid-diagram';
|
||||||
wrapper.innerHTML = DOMPurify.sanitize(svg, {
|
// Mermaid already sanitizes SVG at securityLevel: 'strict' (including
|
||||||
USE_PROFILES: { svg: true, svgFilters: true },
|
// foreignObject HTML labels). A second DOMPurify pass with the SVG-only
|
||||||
});
|
// profile strips those labels and leaves empty flowchart nodes.
|
||||||
|
wrapper.innerHTML = svg;
|
||||||
|
|
||||||
pre.replaceWith(wrapper);
|
pre.replaceWith(wrapper);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { isValidImageFile } from './image-input-utils.js';
|
||||||
|
|
||||||
|
export interface PartitionedFiles {
|
||||||
|
pdfFiles: File[];
|
||||||
|
imageFiles: File[];
|
||||||
|
skipped: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function partitionIncomingFiles(rawFiles: File[]): PartitionedFiles {
|
||||||
|
const pdfFiles: File[] = [];
|
||||||
|
const imageFiles: File[] = [];
|
||||||
|
const skipped: string[] = [];
|
||||||
|
|
||||||
|
for (const f of rawFiles) {
|
||||||
|
if (f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')) {
|
||||||
|
pdfFiles.push(f);
|
||||||
|
} else if (isValidImageFile(f)) {
|
||||||
|
imageFiles.push(f);
|
||||||
|
} else {
|
||||||
|
skipped.push(f.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { pdfFiles, imageFiles, skipped };
|
||||||
|
}
|
||||||
+4
-4
@@ -101,8 +101,8 @@ function drawOcrTextLayer(
|
|||||||
y: transform.y,
|
y: transform.y,
|
||||||
font,
|
font,
|
||||||
size: transform.fontSize,
|
size: transform.fontSize,
|
||||||
color: rgb(1, 0, 0),
|
color: rgb(0, 0, 0),
|
||||||
opacity: 1,
|
opacity: 0,
|
||||||
rotate: lineRotation,
|
rotate: lineRotation,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
@@ -132,8 +132,8 @@ function drawOcrTextLayer(
|
|||||||
y: spaceTransform.y,
|
y: spaceTransform.y,
|
||||||
font,
|
font,
|
||||||
size: spaceTransform.fontSize,
|
size: spaceTransform.fontSize,
|
||||||
color: rgb(1, 0, 0),
|
color: rgb(0, 0, 0),
|
||||||
opacity: 1,
|
opacity: 0,
|
||||||
rotate: lineRotation,
|
rotate: lineRotation,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import type { QpdfInstanceExtended } from '@/types';
|
||||||
|
|
||||||
|
export function parseRangeGroups(
|
||||||
|
input: string,
|
||||||
|
totalPages: number
|
||||||
|
): { groups: number[][]; indices: number[] } {
|
||||||
|
const groups: number[][] = [];
|
||||||
|
const indices: number[] = [];
|
||||||
|
|
||||||
|
for (const range of input.split(',')) {
|
||||||
|
const trimmedRange = range.trim();
|
||||||
|
if (!trimmedRange) continue;
|
||||||
|
|
||||||
|
const groupIndices: number[] = [];
|
||||||
|
if (trimmedRange.includes('-')) {
|
||||||
|
const [start, end] = trimmedRange.split('-').map(Number);
|
||||||
|
if (
|
||||||
|
isNaN(start) ||
|
||||||
|
isNaN(end) ||
|
||||||
|
start < 1 ||
|
||||||
|
end > totalPages ||
|
||||||
|
start > end
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
for (let i = start; i <= end; i++) groupIndices.push(i - 1);
|
||||||
|
} else {
|
||||||
|
const pageNum = Number(trimmedRange);
|
||||||
|
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||||
|
groupIndices.push(pageNum - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupIndices.length > 0) {
|
||||||
|
groups.push(groupIndices);
|
||||||
|
indices.push(...groupIndices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { groups, indices };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function evenOddIndices(
|
||||||
|
choice: 'even' | 'odd',
|
||||||
|
totalPages: number
|
||||||
|
): number[] {
|
||||||
|
const indices: number[] = [];
|
||||||
|
for (let i = 0; i < totalPages; i++) {
|
||||||
|
if (choice === 'even' && (i + 1) % 2 === 0) indices.push(i);
|
||||||
|
if (choice === 'odd' && (i + 1) % 2 !== 0) indices.push(i);
|
||||||
|
}
|
||||||
|
return indices;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function allPagesIndices(totalPages: number): number[] {
|
||||||
|
return Array.from({ length: totalPages }, (_, i) => i);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nTimesGroups(nValue: number, totalPages: number): number[][] {
|
||||||
|
const groups: number[][] = [];
|
||||||
|
const numSplits = Math.ceil(totalPages / nValue);
|
||||||
|
for (let i = 0; i < numSplits; i++) {
|
||||||
|
const startPage = i * nValue;
|
||||||
|
const endPage = Math.min(startPage + nValue - 1, totalPages - 1);
|
||||||
|
groups.push(
|
||||||
|
Array.from(
|
||||||
|
{ length: endPage - startPage + 1 },
|
||||||
|
(_, idx) => startPage + idx
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bookmarkSplitGroups(
|
||||||
|
splitPages: number[],
|
||||||
|
totalPages: number
|
||||||
|
): number[][] {
|
||||||
|
const sorted = [...splitPages].sort((a, b) => a - b);
|
||||||
|
const groups: number[][] = [];
|
||||||
|
for (let i = 0; i < sorted.length; i++) {
|
||||||
|
const startPage = i === 0 ? 0 : sorted[i];
|
||||||
|
const endPage = i < sorted.length - 1 ? sorted[i + 1] - 1 : totalPages - 1;
|
||||||
|
groups.push(
|
||||||
|
Array.from(
|
||||||
|
{ length: endPage - startPage + 1 },
|
||||||
|
(_, idx) => startPage + idx
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function groupFilename(group: number[]): string {
|
||||||
|
const minPage = Math.min(...group) + 1;
|
||||||
|
const maxPage = Math.max(...group) + 1;
|
||||||
|
return minPage === maxPage
|
||||||
|
? `page-${minPage}.pdf`
|
||||||
|
: `pages-${minPage}-${maxPage}.pdf`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uniqueZipName(
|
||||||
|
name: string,
|
||||||
|
usedNames: Map<string, number>
|
||||||
|
): string {
|
||||||
|
const seen = usedNames.get(name);
|
||||||
|
if (seen !== undefined) {
|
||||||
|
usedNames.set(name, seen + 1);
|
||||||
|
return name.replace(/\.pdf$/, `-${seen + 1}.pdf`);
|
||||||
|
}
|
||||||
|
usedNames.set(name, 0);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pagesToSpec(indices: number[]): string {
|
||||||
|
return indices.map((index) => index + 1).join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractPagesWithQpdf(
|
||||||
|
qpdf: QpdfInstanceExtended,
|
||||||
|
inputPath: string,
|
||||||
|
indices: number[],
|
||||||
|
outputPath = '/split-output.pdf'
|
||||||
|
): Uint8Array {
|
||||||
|
const exitCode = qpdf.callMain([
|
||||||
|
inputPath,
|
||||||
|
'--remove-unreferenced-resources=yes',
|
||||||
|
'--pages',
|
||||||
|
'.',
|
||||||
|
pagesToSpec(indices),
|
||||||
|
'--',
|
||||||
|
outputPath,
|
||||||
|
]);
|
||||||
|
try {
|
||||||
|
if (exitCode !== 0 && exitCode !== 3) {
|
||||||
|
throw new Error(`Failed to extract pages (qpdf exit code ${exitCode}).`);
|
||||||
|
}
|
||||||
|
const bytes = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||||
|
if (!bytes || bytes.length === 0) {
|
||||||
|
throw new Error('Page extraction produced an empty PDF.');
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
qpdf.FS.unlink(outputPath);
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.warn('Failed to clean up qpdf output file:', cleanupError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -383,18 +383,18 @@
|
|||||||
class="text-gray-300 text-base sm:text-lg mb-1 sm:mb-2"
|
class="text-gray-300 text-base sm:text-lg mb-1 sm:mb-2"
|
||||||
data-i18n="multiTool.uploadPdfFiles"
|
data-i18n="multiTool.uploadPdfFiles"
|
||||||
>
|
>
|
||||||
Select PDF Files
|
Select PDF or image files
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
class="text-gray-400 text-xs sm:text-sm mb-3 sm:mb-4"
|
class="text-gray-400 text-xs sm:text-sm mb-3 sm:mb-4"
|
||||||
data-i18n="multiTool.dragAndDrop"
|
data-i18n="multiTool.dragAndDrop"
|
||||||
>
|
>
|
||||||
Drag and drop PDF files here, or click to select
|
Drag and drop PDF or image files here, or click to select
|
||||||
</p>
|
</p>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
id="pdf-file-input"
|
id="pdf-file-input"
|
||||||
accept="application/pdf"
|
accept="application/pdf,image/*,.heic,.heif,.jpg,.jpeg,.png,.bmp,.gif,.tiff,.tif,.webp,.svg,.psd,.jp2,.jpx,.jxr,.pnm,.pgm,.pbm,.ppm,.pam"
|
||||||
multiple
|
multiple
|
||||||
class="hidden"
|
class="hidden"
|
||||||
/>
|
/>
|
||||||
|
|||||||
+35
-11
@@ -359,17 +359,41 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="zip-option-wrapper" class="mt-4 flex items-center">
|
<div id="output-mode-wrapper" class="mt-4">
|
||||||
<input
|
<p class="block mb-2 text-sm font-medium text-gray-300">Output</p>
|
||||||
id="download-as-zip"
|
<div class="flex items-center mb-2">
|
||||||
type="checkbox"
|
<input
|
||||||
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-600 ring-offset-gray-800 focus:ring-2"
|
id="output-combine"
|
||||||
/>
|
type="radio"
|
||||||
<label
|
value="combine"
|
||||||
for="download-as-zip"
|
name="split-output-mode"
|
||||||
class="ml-2 text-sm font-medium text-gray-300"
|
checked
|
||||||
>Download as ZIP (for multiple files)</label
|
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-600 ring-offset-gray-800 focus:ring-2"
|
||||||
>
|
/>
|
||||||
|
<label
|
||||||
|
for="output-combine"
|
||||||
|
class="ml-2 text-sm font-medium text-gray-300"
|
||||||
|
>Single combined PDF</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input
|
||||||
|
id="output-separate"
|
||||||
|
type="radio"
|
||||||
|
value="separate"
|
||||||
|
name="split-output-mode"
|
||||||
|
class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-600 ring-offset-gray-800 focus:ring-2"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="output-separate"
|
||||||
|
id="output-separate-label"
|
||||||
|
class="ml-2 text-sm font-medium text-gray-300"
|
||||||
|
>One PDF per range</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-xs text-gray-400">
|
||||||
|
Multiple files are downloaded together as a ZIP.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="process-btn" class="btn-gradient w-full mt-6">
|
<button id="process-btn" class="btn-gradient w-full mt-6">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { resolveFontUrl } from '../js/utils/font-loader';
|
|||||||
describe('font-loader', () => {
|
describe('font-loader', () => {
|
||||||
it('uses the default public font URL when no offline font base URL is configured', () => {
|
it('uses the default public font URL when no offline font base URL is configured', () => {
|
||||||
expect(resolveFontUrl('Noto Sans', {})).toBe(
|
expect(resolveFontUrl('Noto Sans', {})).toBe(
|
||||||
'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSans/NotoSans-Regular.ttf'
|
'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSans/NotoSans-Regular.ttf'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
|
||||||
|
vi.mock('heic2any', () => ({
|
||||||
|
default: vi.fn(async ({ blob }: { blob: Blob }) => {
|
||||||
|
const bytes = new Uint8Array(await blob.arrayBuffer());
|
||||||
|
return new Blob([bytes], { type: 'image/png' });
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockEmbedJpg = vi.fn();
|
||||||
|
const mockEmbedPng = vi.fn();
|
||||||
|
const mockDrawImage = vi.fn();
|
||||||
|
const mockAddPage = vi.fn();
|
||||||
|
const mockSave = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('pdf-lib', () => {
|
||||||
|
return {
|
||||||
|
PDFDocument: {
|
||||||
|
create: vi.fn(async () => ({
|
||||||
|
embedJpg: mockEmbedJpg,
|
||||||
|
embedPng: mockEmbedPng,
|
||||||
|
addPage: mockAddPage,
|
||||||
|
save: mockSave,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
import {
|
||||||
|
normalizeImageToEmbeddable,
|
||||||
|
convertImagesToPdfFile,
|
||||||
|
} from '../js/utils/images-to-pdf-lib';
|
||||||
|
|
||||||
|
function makeFile(name: string, type: string, bytes = [1, 2, 3]): File {
|
||||||
|
return new File([new Uint8Array(bytes)], name, { type });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('normalizeImageToEmbeddable', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockEmbedJpg.mockReset();
|
||||||
|
mockEmbedPng.mockReset();
|
||||||
|
mockDrawImage.mockReset();
|
||||||
|
mockAddPage.mockReset();
|
||||||
|
mockSave.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes through JPG bytes unchanged when MIME is image/jpeg', async () => {
|
||||||
|
const file = makeFile('photo.jpg', 'image/jpeg', [0xff, 0xd8, 0xff, 0xe0]);
|
||||||
|
const result = await normalizeImageToEmbeddable(file);
|
||||||
|
expect(result.format).toBe('jpg');
|
||||||
|
expect(Array.from(result.bytes)).toEqual([0xff, 0xd8, 0xff, 0xe0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes through JPG by extension when MIME is missing', async () => {
|
||||||
|
const file = makeFile('photo.jpeg', '', [0xff, 0xd8]);
|
||||||
|
const result = await normalizeImageToEmbeddable(file);
|
||||||
|
expect(result.format).toBe('jpg');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes through PNG bytes unchanged when MIME is image/png', async () => {
|
||||||
|
const file = makeFile('scan.png', 'image/png', [0x89, 0x50, 0x4e, 0x47]);
|
||||||
|
const result = await normalizeImageToEmbeddable(file);
|
||||||
|
expect(result.format).toBe('png');
|
||||||
|
expect(Array.from(result.bytes)).toEqual([0x89, 0x50, 0x4e, 0x47]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes through PNG by uppercase extension when MIME is missing', async () => {
|
||||||
|
const file = makeFile('SCAN.PNG', '', [0x89, 0x50]);
|
||||||
|
const result = await normalizeImageToEmbeddable(file);
|
||||||
|
expect(result.format).toBe('png');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('canvas-decode path (non-JPG/PNG formats)', () => {
|
||||||
|
const originalCreateElement = document.createElement.bind(document);
|
||||||
|
const originalImage = globalThis.Image;
|
||||||
|
const originalCreateObjectURL = URL.createObjectURL;
|
||||||
|
const originalRevokeObjectURL = URL.revokeObjectURL;
|
||||||
|
const revokeSpy = vi.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
revokeSpy.mockReset();
|
||||||
|
|
||||||
|
URL.createObjectURL = vi.fn(() => 'blob:test');
|
||||||
|
URL.revokeObjectURL = revokeSpy;
|
||||||
|
|
||||||
|
globalThis.Image = class {
|
||||||
|
naturalWidth = 100;
|
||||||
|
naturalHeight = 50;
|
||||||
|
onload: (() => void) | null = null;
|
||||||
|
onerror: (() => void) | null = null;
|
||||||
|
set src(_: string) {
|
||||||
|
setTimeout(() => this.onload?.(), 0);
|
||||||
|
}
|
||||||
|
} as unknown as typeof Image;
|
||||||
|
|
||||||
|
document.createElement = ((tagName: string) => {
|
||||||
|
if (tagName !== 'canvas') {
|
||||||
|
return originalCreateElement(tagName);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
getContext: vi.fn(() => ({ drawImage: vi.fn() })),
|
||||||
|
toBlob: vi.fn((cb: (b: Blob) => void) => {
|
||||||
|
cb(new Blob([new Uint8Array([0x89, 0x50])], { type: 'image/png' }));
|
||||||
|
}),
|
||||||
|
} as unknown as HTMLCanvasElement;
|
||||||
|
}) as typeof document.createElement;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
document.createElement = originalCreateElement;
|
||||||
|
globalThis.Image = originalImage;
|
||||||
|
URL.createObjectURL = originalCreateObjectURL;
|
||||||
|
URL.revokeObjectURL = originalRevokeObjectURL;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decodes a BMP through canvas and returns PNG bytes', async () => {
|
||||||
|
const file = makeFile('scan.bmp', 'image/bmp');
|
||||||
|
const result = await normalizeImageToEmbeddable(file);
|
||||||
|
expect(result.format).toBe('png');
|
||||||
|
expect(Array.from(result.bytes)).toEqual([0x89, 0x50]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decodes a TIFF through canvas', async () => {
|
||||||
|
const file = makeFile('page.tiff', 'image/tiff');
|
||||||
|
const result = await normalizeImageToEmbeddable(file);
|
||||||
|
expect(result.format).toBe('png');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('revokes the object URL after success', async () => {
|
||||||
|
const file = makeFile('scan.bmp', 'image/bmp');
|
||||||
|
await normalizeImageToEmbeddable(file);
|
||||||
|
expect(revokeSpy).toHaveBeenCalledWith('blob:test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('revokes the object URL even on Image decode error', async () => {
|
||||||
|
globalThis.Image = class {
|
||||||
|
naturalWidth = 0;
|
||||||
|
naturalHeight = 0;
|
||||||
|
onload: (() => void) | null = null;
|
||||||
|
onerror: (() => void) | null = null;
|
||||||
|
set src(_: string) {
|
||||||
|
setTimeout(() => this.onerror?.(), 0);
|
||||||
|
}
|
||||||
|
} as unknown as typeof Image;
|
||||||
|
|
||||||
|
const file = makeFile('broken.gif', 'image/gif');
|
||||||
|
await expect(normalizeImageToEmbeddable(file)).rejects.toThrow(
|
||||||
|
/Failed to decode image/
|
||||||
|
);
|
||||||
|
expect(revokeSpy).toHaveBeenCalledWith('blob:test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects when canvas getContext returns null', async () => {
|
||||||
|
document.createElement = ((tagName: string) => {
|
||||||
|
if (tagName !== 'canvas') {
|
||||||
|
return originalCreateElement(tagName);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
getContext: vi.fn(() => null),
|
||||||
|
toBlob: vi.fn(),
|
||||||
|
} as unknown as HTMLCanvasElement;
|
||||||
|
}) as typeof document.createElement;
|
||||||
|
|
||||||
|
const file = makeFile('scan.bmp', 'image/bmp');
|
||||||
|
await expect(normalizeImageToEmbeddable(file)).rejects.toThrow(
|
||||||
|
/Canvas context unavailable/
|
||||||
|
);
|
||||||
|
expect(revokeSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects when canvas.toBlob returns null', async () => {
|
||||||
|
document.createElement = ((tagName: string) => {
|
||||||
|
if (tagName !== 'canvas') {
|
||||||
|
return originalCreateElement(tagName);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
getContext: vi.fn(() => ({ drawImage: vi.fn() })),
|
||||||
|
toBlob: vi.fn((cb: (b: Blob | null) => void) => cb(null)),
|
||||||
|
} as unknown as HTMLCanvasElement;
|
||||||
|
}) as typeof document.createElement;
|
||||||
|
|
||||||
|
const file = makeFile('scan.bmp', 'image/bmp');
|
||||||
|
await expect(normalizeImageToEmbeddable(file)).rejects.toThrow(
|
||||||
|
/Canvas toBlob failed/
|
||||||
|
);
|
||||||
|
expect(revokeSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('convertImagesToPdfFile', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockEmbedJpg.mockReset();
|
||||||
|
mockEmbedPng.mockReset();
|
||||||
|
mockDrawImage.mockReset();
|
||||||
|
mockAddPage.mockReset();
|
||||||
|
mockSave.mockReset();
|
||||||
|
|
||||||
|
mockEmbedJpg.mockResolvedValue({ width: 300, height: 200 });
|
||||||
|
mockEmbedPng.mockResolvedValue({ width: 600, height: 400 });
|
||||||
|
mockAddPage.mockReturnValue({ drawImage: mockDrawImage });
|
||||||
|
mockSave.mockResolvedValue(new Uint8Array([0x25, 0x50, 0x44, 0x46]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('embeds a single JPG as one page and names file after image', async () => {
|
||||||
|
const file = makeFile('vacation.jpg', 'image/jpeg', [0xff, 0xd8]);
|
||||||
|
const result = await convertImagesToPdfFile([file]);
|
||||||
|
|
||||||
|
expect(mockEmbedJpg).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockEmbedPng).not.toHaveBeenCalled();
|
||||||
|
expect(mockAddPage).toHaveBeenCalledWith([300, 200]);
|
||||||
|
expect(mockDrawImage).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ width: 300, height: 200 }),
|
||||||
|
{ x: 0, y: 0, width: 300, height: 200 }
|
||||||
|
);
|
||||||
|
expect(result.name).toBe('vacation.pdf');
|
||||||
|
expect(result.type).toBe('application/pdf');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('embeds a single PNG and names file after image', async () => {
|
||||||
|
const file = makeFile('scan.png', 'image/png', [0x89, 0x50]);
|
||||||
|
const result = await convertImagesToPdfFile([file]);
|
||||||
|
|
||||||
|
expect(mockEmbedPng).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockEmbedJpg).not.toHaveBeenCalled();
|
||||||
|
expect(mockAddPage).toHaveBeenCalledWith([600, 400]);
|
||||||
|
expect(result.name).toBe('scan.pdf');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('embeds multiple images and uses generic filename', async () => {
|
||||||
|
const files = [
|
||||||
|
makeFile('a.jpg', 'image/jpeg'),
|
||||||
|
makeFile('b.png', 'image/png'),
|
||||||
|
makeFile('c.jpg', 'image/jpeg'),
|
||||||
|
];
|
||||||
|
const result = await convertImagesToPdfFile(files);
|
||||||
|
|
||||||
|
expect(mockEmbedJpg).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockEmbedPng).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockAddPage).toHaveBeenCalledTimes(3);
|
||||||
|
expect(result.name).toMatch(/^images-\d+\.pdf$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('produces a PDF File with correct MIME type', async () => {
|
||||||
|
const file = makeFile('a.jpg', 'image/jpeg');
|
||||||
|
const result = await convertImagesToPdfFile([file]);
|
||||||
|
expect(result.type).toBe('application/pdf');
|
||||||
|
expect(result).toBeInstanceOf(File);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('strips the original extension when generating single-file name', async () => {
|
||||||
|
const file = makeFile('photo.JPEG', 'image/jpeg');
|
||||||
|
const result = await convertImagesToPdfFile([file]);
|
||||||
|
expect(result.name).toBe('photo.pdf');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('propagates errors from pdf-lib embed step', async () => {
|
||||||
|
mockEmbedJpg.mockRejectedValueOnce(new Error('embed failed'));
|
||||||
|
const file = makeFile('bad.jpg', 'image/jpeg');
|
||||||
|
await expect(convertImagesToPdfFile([file])).rejects.toThrow(
|
||||||
|
/embed failed/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import DOMPurify, { type Config } from 'dompurify';
|
||||||
|
|
||||||
|
// Broken when applied after mermaid.render() — strips foreignObject labels.
|
||||||
|
const BENTOPDF_SVG_ONLY_SANITIZE_OPTIONS: Config = {
|
||||||
|
USE_PROFILES: { svg: true, svgFilters: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Matches Mermaid 11 strict-mode output sanitization in mermaid.render(). */
|
||||||
|
const MERMAID_STRICT_SVG_SANITIZE_OPTIONS: Config = {
|
||||||
|
ADD_TAGS: ['foreignobject'],
|
||||||
|
ADD_ATTR: ['dominant-baseline'],
|
||||||
|
HTML_INTEGRATION_POINTS: { foreignobject: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Markdown to PDF — Mermaid SVG sanitization', () => {
|
||||||
|
it('documents that SVG-only DOMPurify strips foreignObject labels (regression guard)', () => {
|
||||||
|
const htmlLabelsSvg = `<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g class="node">
|
||||||
|
<rect width="100" height="50"/>
|
||||||
|
<foreignObject width="100" height="50">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex">
|
||||||
|
<span>Start</span>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
</g>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
const clean = DOMPurify.sanitize(
|
||||||
|
htmlLabelsSvg,
|
||||||
|
BENTOPDF_SVG_ONLY_SANITIZE_OPTIONS
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(clean).not.toContain('Start');
|
||||||
|
expect(clean).not.toContain('foreignObject');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves foreignObject labels with Mermaid strict sanitizer settings', () => {
|
||||||
|
const htmlLabelsSvg = `<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<foreignObject width="120" height="40">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml" style="display:flex">
|
||||||
|
<span class="nodeLabel">Start</span>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
const clean = DOMPurify.sanitize(
|
||||||
|
htmlLabelsSvg,
|
||||||
|
MERMAID_STRICT_SVG_SANITIZE_OPTIONS
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(clean).toContain('Start');
|
||||||
|
expect(clean).toContain('foreignObject');
|
||||||
|
expect(clean).toContain('nodeLabel');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves native SVG text labels under SVG-only profile', () => {
|
||||||
|
const svgTextLabels = `<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g class="node">
|
||||||
|
<rect width="100" height="50"/>
|
||||||
|
<text x="10" y="20" fill="#333">Start</text>
|
||||||
|
</g>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
const clean = DOMPurify.sanitize(
|
||||||
|
svgTextLabels,
|
||||||
|
BENTOPDF_SVG_ONLY_SANITIZE_OPTIONS
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(clean).toContain('Start');
|
||||||
|
expect(clean).toContain('<text');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
|
||||||
|
vi.mock('heic2any', () => ({
|
||||||
|
default: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { partitionIncomingFiles } from '../js/utils/multi-tool-file-input';
|
||||||
|
|
||||||
|
function makeFile(name: string, type: string): File {
|
||||||
|
return new File([new Uint8Array([1, 2, 3])], name, { type });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('partitionIncomingFiles', () => {
|
||||||
|
it('routes PDFs by MIME type', () => {
|
||||||
|
const result = partitionIncomingFiles([
|
||||||
|
makeFile('a.pdf', 'application/pdf'),
|
||||||
|
makeFile('b.pdf', 'application/pdf'),
|
||||||
|
]);
|
||||||
|
expect(result.pdfFiles).toHaveLength(2);
|
||||||
|
expect(result.imageFiles).toHaveLength(0);
|
||||||
|
expect(result.skipped).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('routes PDFs by extension when MIME is missing', () => {
|
||||||
|
const result = partitionIncomingFiles([makeFile('doc.pdf', '')]);
|
||||||
|
expect(result.pdfFiles).toHaveLength(1);
|
||||||
|
expect(result.imageFiles).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('routes PDFs with uppercase extension', () => {
|
||||||
|
const result = partitionIncomingFiles([makeFile('SCAN.PDF', '')]);
|
||||||
|
expect(result.pdfFiles).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('routes images by MIME type', () => {
|
||||||
|
const result = partitionIncomingFiles([
|
||||||
|
makeFile('a.png', 'image/png'),
|
||||||
|
makeFile('b.jpg', 'image/jpeg'),
|
||||||
|
makeFile('c.webp', 'image/webp'),
|
||||||
|
]);
|
||||||
|
expect(result.imageFiles).toHaveLength(3);
|
||||||
|
expect(result.pdfFiles).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('routes images by extension when MIME is missing (HEIC, BMP, TIFF)', () => {
|
||||||
|
const result = partitionIncomingFiles([
|
||||||
|
makeFile('photo.heic', ''),
|
||||||
|
makeFile('scan.bmp', ''),
|
||||||
|
makeFile('page.tiff', ''),
|
||||||
|
]);
|
||||||
|
expect(result.imageFiles).toHaveLength(3);
|
||||||
|
expect(result.skipped).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('splits a mixed batch into PDFs, images, and skipped', () => {
|
||||||
|
const result = partitionIncomingFiles([
|
||||||
|
makeFile('doc.pdf', 'application/pdf'),
|
||||||
|
makeFile('photo.png', 'image/png'),
|
||||||
|
makeFile('readme.txt', 'text/plain'),
|
||||||
|
makeFile('archive.zip', 'application/zip'),
|
||||||
|
]);
|
||||||
|
expect(result.pdfFiles).toHaveLength(1);
|
||||||
|
expect(result.imageFiles).toHaveLength(1);
|
||||||
|
expect(result.skipped).toEqual(['readme.txt', 'archive.zip']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves original File order within each bucket', () => {
|
||||||
|
const a = makeFile('a.pdf', 'application/pdf');
|
||||||
|
const b = makeFile('b.png', 'image/png');
|
||||||
|
const c = makeFile('c.pdf', 'application/pdf');
|
||||||
|
const d = makeFile('d.jpg', 'image/jpeg');
|
||||||
|
|
||||||
|
const result = partitionIncomingFiles([a, b, c, d]);
|
||||||
|
expect(result.pdfFiles).toEqual([a, c]);
|
||||||
|
expect(result.imageFiles).toEqual([b, d]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty buckets for empty input', () => {
|
||||||
|
const result = partitionIncomingFiles([]);
|
||||||
|
expect(result.pdfFiles).toHaveLength(0);
|
||||||
|
expect(result.imageFiles).toHaveLength(0);
|
||||||
|
expect(result.skipped).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips files with neither matching MIME nor extension', () => {
|
||||||
|
const result = partitionIncomingFiles([
|
||||||
|
makeFile('script.js', 'application/javascript'),
|
||||||
|
makeFile('data.csv', 'text/csv'),
|
||||||
|
]);
|
||||||
|
expect(result.skipped).toHaveLength(2);
|
||||||
|
expect(result.pdfFiles).toHaveLength(0);
|
||||||
|
expect(result.imageFiles).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,364 @@
|
|||||||
|
import { describe, it, expect, beforeAll } from 'vitest';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import createModule from '@neslinesli93/qpdf-wasm';
|
||||||
|
import { PDFDocument } from 'pdf-lib';
|
||||||
|
import type { QpdfInstanceExtended } from '@/types';
|
||||||
|
import {
|
||||||
|
parseRangeGroups,
|
||||||
|
evenOddIndices,
|
||||||
|
allPagesIndices,
|
||||||
|
nTimesGroups,
|
||||||
|
bookmarkSplitGroups,
|
||||||
|
groupFilename,
|
||||||
|
uniqueZipName,
|
||||||
|
pagesToSpec,
|
||||||
|
extractPagesWithQpdf,
|
||||||
|
} from '@/js/utils/split-pdf-helpers';
|
||||||
|
|
||||||
|
const FIXTURES = path.resolve(__dirname, './fixtures');
|
||||||
|
|
||||||
|
describe('split-pdf-helpers (pure planning)', () => {
|
||||||
|
describe('parseRangeGroups', () => {
|
||||||
|
it('parses a single page into one group', () => {
|
||||||
|
expect(parseRangeGroups('3', 10)).toEqual({
|
||||||
|
groups: [[2]],
|
||||||
|
indices: [2],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses a hyphen range into one consecutive group', () => {
|
||||||
|
expect(parseRangeGroups('2-5', 10)).toEqual({
|
||||||
|
groups: [[1, 2, 3, 4]],
|
||||||
|
indices: [1, 2, 3, 4],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps disjoint ranges as separate groups, indices flattened in order', () => {
|
||||||
|
expect(parseRangeGroups('1-5, 8, 11-13', 20)).toEqual({
|
||||||
|
groups: [[0, 1, 2, 3, 4], [7], [10, 11, 12]],
|
||||||
|
indices: [0, 1, 2, 3, 4, 7, 10, 11, 12],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves the user-entered order (does not sort)', () => {
|
||||||
|
expect(parseRangeGroups('8, 1-2', 10)).toEqual({
|
||||||
|
groups: [[7], [0, 1]],
|
||||||
|
indices: [7, 0, 1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips whitespace and empty parts', () => {
|
||||||
|
expect(parseRangeGroups(' 1 , , 3-4 ', 10)).toEqual({
|
||||||
|
groups: [[0], [2, 3]],
|
||||||
|
indices: [0, 2, 3],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips out-of-bounds, reversed, and non-numeric ranges', () => {
|
||||||
|
expect(parseRangeGroups('0-2, 9-5, abc, 3', 4)).toEqual({
|
||||||
|
groups: [[2]],
|
||||||
|
indices: [2],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips a range whose end exceeds the page count', () => {
|
||||||
|
expect(parseRangeGroups('1-100', 5)).toEqual({ groups: [], indices: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty for an all-invalid input', () => {
|
||||||
|
expect(parseRangeGroups('99, abc', 5)).toEqual({
|
||||||
|
groups: [],
|
||||||
|
indices: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('evenOddIndices', () => {
|
||||||
|
it('selects even pages (1-based) as 0-based indices', () => {
|
||||||
|
expect(evenOddIndices('even', 6)).toEqual([1, 3, 5]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects odd pages', () => {
|
||||||
|
expect(evenOddIndices('odd', 6)).toEqual([0, 2, 4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles odd total page counts', () => {
|
||||||
|
expect(evenOddIndices('even', 5)).toEqual([1, 3]);
|
||||||
|
expect(evenOddIndices('odd', 5)).toEqual([0, 2, 4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles a single-page document', () => {
|
||||||
|
expect(evenOddIndices('odd', 1)).toEqual([0]);
|
||||||
|
expect(evenOddIndices('even', 1)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('allPagesIndices', () => {
|
||||||
|
it('returns every page index', () => {
|
||||||
|
expect(allPagesIndices(4)).toEqual([0, 1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty for zero pages', () => {
|
||||||
|
expect(allPagesIndices(0)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('nTimesGroups', () => {
|
||||||
|
it('splits evenly when divisible', () => {
|
||||||
|
expect(nTimesGroups(2, 6)).toEqual([
|
||||||
|
[0, 1],
|
||||||
|
[2, 3],
|
||||||
|
[4, 5],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('puts the remainder in the final group', () => {
|
||||||
|
expect(nTimesGroups(2, 5)).toEqual([[0, 1], [2, 3], [4]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a single group when N >= total', () => {
|
||||||
|
expect(nTimesGroups(10, 3)).toEqual([[0, 1, 2]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('produces one group per page when N is 1', () => {
|
||||||
|
expect(nTimesGroups(1, 3)).toEqual([[0], [1], [2]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('bookmarkSplitGroups', () => {
|
||||||
|
it('builds consecutive ranges from boundary pages, first section starts at 0', () => {
|
||||||
|
expect(bookmarkSplitGroups([2, 4], 8)).toEqual([
|
||||||
|
[0, 1, 2, 3],
|
||||||
|
[4, 5, 6, 7],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts unsorted boundaries before grouping', () => {
|
||||||
|
expect(bookmarkSplitGroups([4, 2], 8)).toEqual([
|
||||||
|
[0, 1, 2, 3],
|
||||||
|
[4, 5, 6, 7],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats a lone boundary as one whole-document group (preserves existing behavior)', () => {
|
||||||
|
expect(bookmarkSplitGroups([3], 5)).toEqual([[0, 1, 2, 3, 4]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('splits at every boundary after the first', () => {
|
||||||
|
expect(bookmarkSplitGroups([2, 4, 6], 8)).toEqual([
|
||||||
|
[0, 1, 2, 3],
|
||||||
|
[4, 5],
|
||||||
|
[6, 7],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('groupFilename', () => {
|
||||||
|
it('names a single-page group page-N.pdf', () => {
|
||||||
|
expect(groupFilename([4])).toBe('page-5.pdf');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('names a multi-page group pages-min-max.pdf', () => {
|
||||||
|
expect(groupFilename([0, 1, 2, 3, 4])).toBe('pages-1-5.pdf');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('uniqueZipName', () => {
|
||||||
|
it('returns the name unchanged on first use', () => {
|
||||||
|
const used = new Map<string, number>();
|
||||||
|
expect(uniqueZipName('page-1.pdf', used)).toBe('page-1.pdf');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('suffixes collisions instead of overwriting', () => {
|
||||||
|
const used = new Map<string, number>();
|
||||||
|
expect(uniqueZipName('page-1.pdf', used)).toBe('page-1.pdf');
|
||||||
|
expect(uniqueZipName('page-1.pdf', used)).toBe('page-1-1.pdf');
|
||||||
|
expect(uniqueZipName('page-1.pdf', used)).toBe('page-1-2.pdf');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('pagesToSpec', () => {
|
||||||
|
it('converts 0-based indices to a 1-based comma-separated qpdf spec', () => {
|
||||||
|
expect(pagesToSpec([0, 1, 7])).toBe('1,2,8');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves order for non-ascending selections', () => {
|
||||||
|
expect(pagesToSpec([7, 0, 1])).toBe('8,1,2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('extractPagesWithQpdf (error handling, stubbed)', () => {
|
||||||
|
function stubQpdf(
|
||||||
|
exitCode: number,
|
||||||
|
output: Uint8Array
|
||||||
|
): QpdfInstanceExtended {
|
||||||
|
return {
|
||||||
|
callMain: () => exitCode,
|
||||||
|
FS: {
|
||||||
|
mkdir: () => {},
|
||||||
|
mount: () => {},
|
||||||
|
unmount: () => {},
|
||||||
|
writeFile: () => {},
|
||||||
|
readFile: () => output,
|
||||||
|
unlink: () => {},
|
||||||
|
analyzePath: () => ({ exists: true }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
it('throws on a hard qpdf error (exit code 2)', () => {
|
||||||
|
const qpdf = stubQpdf(2, new Uint8Array([1, 2, 3]));
|
||||||
|
expect(() => extractPagesWithQpdf(qpdf, '/in.pdf', [0])).toThrow(
|
||||||
|
/exit code 2/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when qpdf produces an empty file', () => {
|
||||||
|
const qpdf = stubQpdf(0, new Uint8Array());
|
||||||
|
expect(() => extractPagesWithQpdf(qpdf, '/in.pdf', [0])).toThrow(
|
||||||
|
/empty PDF/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts exit code 3 (warnings) with valid output', () => {
|
||||||
|
const out = new Uint8Array([37, 80, 68, 70]);
|
||||||
|
const qpdf = stubQpdf(3, out);
|
||||||
|
expect(extractPagesWithQpdf(qpdf, '/in.pdf', [0])).toBe(out);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('split modes end-to-end with real qpdf', () => {
|
||||||
|
let qpdf: QpdfInstanceExtended;
|
||||||
|
|
||||||
|
async function makePdf(pageCount: number): Promise<Uint8Array> {
|
||||||
|
const doc = await PDFDocument.create();
|
||||||
|
for (let i = 0; i < pageCount; i++) doc.addPage([100 + i, 200]);
|
||||||
|
return new Uint8Array(await doc.save());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function originalIndices(bytes: Uint8Array): Promise<number[]> {
|
||||||
|
const doc = await PDFDocument.load(bytes);
|
||||||
|
return doc.getPages().map((p) => Math.round(p.getWidth()) - 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extract(source: Uint8Array, indices: number[]): Uint8Array {
|
||||||
|
qpdf.FS.writeFile('/in.pdf', source);
|
||||||
|
try {
|
||||||
|
return extractPagesWithQpdf(qpdf, '/in.pdf', indices);
|
||||||
|
} finally {
|
||||||
|
qpdf.FS.unlink('/in.pdf');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const wasmBinary = fs.readFileSync(
|
||||||
|
path.resolve(
|
||||||
|
process.cwd(),
|
||||||
|
'node_modules/@neslinesli93/qpdf-wasm/dist/qpdf.wasm'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
qpdf = await (
|
||||||
|
createModule as unknown as (o: object) => Promise<QpdfInstanceExtended>
|
||||||
|
)({ wasmBinary, noInitialRun: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('range (combine): disjoint ranges -> one PDF with the right pages in order', async () => {
|
||||||
|
const src = await makePdf(20);
|
||||||
|
const { indices } = parseRangeGroups('1-5, 8, 11-13', 20);
|
||||||
|
const out = extract(src, indices);
|
||||||
|
expect(await originalIndices(out)).toEqual([0, 1, 2, 3, 4, 7, 10, 11, 12]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('range (separate): each range group -> its own PDF', async () => {
|
||||||
|
const src = await makePdf(20);
|
||||||
|
const { groups } = parseRangeGroups('1-5, 8, 11-13', 20);
|
||||||
|
const outputs = groups.map((g) => extract(src, g));
|
||||||
|
const counts = await Promise.all(
|
||||||
|
outputs.map(async (o) => (await PDFDocument.load(o)).getPageCount())
|
||||||
|
);
|
||||||
|
expect(counts).toEqual([5, 1, 3]);
|
||||||
|
expect(groups.map(groupFilename)).toEqual([
|
||||||
|
'pages-1-5.pdf',
|
||||||
|
'page-8.pdf',
|
||||||
|
'pages-11-13.pdf',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('even-odd: extracts the correct pages', async () => {
|
||||||
|
const src = await makePdf(6);
|
||||||
|
expect(
|
||||||
|
await originalIndices(extract(src, evenOddIndices('even', 6)))
|
||||||
|
).toEqual([1, 3, 5]);
|
||||||
|
expect(
|
||||||
|
await originalIndices(extract(src, evenOddIndices('odd', 6)))
|
||||||
|
).toEqual([0, 2, 4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('all: one single-page PDF per page', async () => {
|
||||||
|
const src = await makePdf(4);
|
||||||
|
const groups = allPagesIndices(4).map((i) => [i]);
|
||||||
|
const outputs = groups.map((g) => extract(src, g));
|
||||||
|
const ids = await Promise.all(outputs.map((o) => originalIndices(o)));
|
||||||
|
expect(ids).toEqual([[0], [1], [2], [3]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('n-times: chunks of N pages with remainder in the last file', async () => {
|
||||||
|
const src = await makePdf(5);
|
||||||
|
const groups = nTimesGroups(2, 5);
|
||||||
|
const outputs = groups.map((g) => extract(src, g));
|
||||||
|
const ids = await Promise.all(outputs.map((o) => originalIndices(o)));
|
||||||
|
expect(ids).toEqual([[0, 1], [2, 3], [4]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('bookmarks: boundary pages -> consecutive section PDFs', async () => {
|
||||||
|
const src = await makePdf(8);
|
||||||
|
const groups = bookmarkSplitGroups([2, 4], 8);
|
||||||
|
const outputs = groups.map((g) => extract(src, g));
|
||||||
|
const ids = await Promise.all(outputs.map((o) => originalIndices(o)));
|
||||||
|
expect(ids).toEqual([
|
||||||
|
[0, 1, 2, 3],
|
||||||
|
[4, 5, 6, 7],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('produces a single valid one-page PDF for a single index', async () => {
|
||||||
|
const src = await makePdf(3);
|
||||||
|
const out = extract(src, [1]);
|
||||||
|
const doc = await PDFDocument.load(out);
|
||||||
|
expect(doc.getPageCount()).toBe(1);
|
||||||
|
expect(Math.round(doc.getPage(0).getWidth())).toBe(101);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fixes resource bloat: a single-page extract is far smaller than pdf-lib copyPages', async () => {
|
||||||
|
const src = new Uint8Array(
|
||||||
|
fs.readFileSync(path.join(FIXTURES, 'shared-resources.pdf'))
|
||||||
|
);
|
||||||
|
|
||||||
|
const srcDoc = await PDFDocument.load(src);
|
||||||
|
const plDoc = await PDFDocument.create();
|
||||||
|
(await plDoc.copyPages(srcDoc, [0])).forEach((p) => plDoc.addPage(p));
|
||||||
|
const pdfLibSize = (await plDoc.save()).length;
|
||||||
|
|
||||||
|
const qpdfSize = extract(src, [0]).length;
|
||||||
|
|
||||||
|
expect(qpdfSize).toBeLessThan(pdfLibSize * 0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves the document outline that pdf-lib copyPages drops', async () => {
|
||||||
|
const src = new Uint8Array(
|
||||||
|
fs.readFileSync(path.join(FIXTURES, 'bookmarked.pdf'))
|
||||||
|
);
|
||||||
|
|
||||||
|
const srcDoc = await PDFDocument.load(src);
|
||||||
|
const plDoc = await PDFDocument.create();
|
||||||
|
(await plDoc.copyPages(srcDoc, [0, 1, 2])).forEach((p) => plDoc.addPage(p));
|
||||||
|
const pdfLibOut = new TextDecoder('latin1').decode(await plDoc.save());
|
||||||
|
|
||||||
|
const qpdfOut = new TextDecoder('latin1').decode(extract(src, [0, 1, 2]));
|
||||||
|
|
||||||
|
expect(pdfLibOut).not.toContain('/Outlines');
|
||||||
|
expect(qpdfOut).toContain('/Outlines');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -34,6 +34,7 @@ const SUPPORTED_LANGUAGES = [
|
|||||||
'ko',
|
'ko',
|
||||||
'ja',
|
'ja',
|
||||||
'uk',
|
'uk',
|
||||||
|
'sk',
|
||||||
] as const;
|
] as const;
|
||||||
const LANG_REGEX = new RegExp(
|
const LANG_REGEX = new RegExp(
|
||||||
`^/(${SUPPORTED_LANGUAGES.join('|')})(?:/(.*))?$`
|
`^/(${SUPPORTED_LANGUAGES.join('|')})(?:/(.*))?$`
|
||||||
|
|||||||
Reference in new issue
Block a user