mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
Merge remote-tracking branch 'origin/main' into feature/react-overhaul
This commit is contained in:
@@ -115,7 +115,7 @@ labels:
|
|||||||
- '.editorconfig'
|
- '.editorconfig'
|
||||||
- '.pre-commit-config'
|
- '.pre-commit-config'
|
||||||
- '.github/workflows/pre_commit.yml'
|
- '.github/workflows/pre_commit.yml'
|
||||||
- 'HowToAddNewLanguage.md'
|
- 'devGuide/.*'
|
||||||
|
|
||||||
- label: 'Test'
|
- label: 'Test'
|
||||||
files:
|
files:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Description of Changes
|
# Description of Changes
|
||||||
|
|
||||||
|
<!--
|
||||||
Please provide a summary of the changes, including:
|
Please provide a summary of the changes, including:
|
||||||
|
|
||||||
- What was changed
|
- What was changed
|
||||||
@@ -7,6 +8,7 @@ Please provide a summary of the changes, including:
|
|||||||
- Any challenges encountered
|
- Any challenges encountered
|
||||||
|
|
||||||
Closes #(issue_number)
|
Closes #(issue_number)
|
||||||
|
-->
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -15,15 +17,15 @@ Closes #(issue_number)
|
|||||||
### General
|
### General
|
||||||
|
|
||||||
- [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
|
- [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
|
||||||
- [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable)
|
- [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable)
|
||||||
- [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md) (if applicable)
|
- [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable)
|
||||||
- [ ] I have performed a self-review of my own code
|
- [ ] I have performed a self-review of my own code
|
||||||
- [ ] My changes generate no new warnings
|
- [ ] My changes generate no new warnings
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
- [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed)
|
- [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed)
|
||||||
- [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only)
|
- [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only)
|
||||||
|
|
||||||
### UI Changes (if applicable)
|
### UI Changes (if applicable)
|
||||||
|
|
||||||
@@ -31,4 +33,4 @@ Closes #(issue_number)
|
|||||||
|
|
||||||
### Testing (if applicable)
|
### Testing (if applicable)
|
||||||
|
|
||||||
- [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing) for more details.
|
- [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# This file is autogenerated by pip-compile with Python 3.10
|
# This file is autogenerated by pip-compile with Python 3.10
|
||||||
# by the following command:
|
# by the following command:
|
||||||
#
|
#
|
||||||
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_pre_commit.txt' '.github\scripts\requirements_pre_commit.in'
|
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_pre_commit.txt' --strip-extras '.github\scripts\requirements_pre_commit.in'
|
||||||
#
|
#
|
||||||
cfgv==3.4.0 \
|
cfgv==3.4.0 \
|
||||||
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
|
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
|
||||||
@@ -12,25 +12,25 @@ distlib==0.3.9 \
|
|||||||
--hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \
|
--hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \
|
||||||
--hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403
|
--hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
filelock==3.16.1 \
|
filelock==3.18.0 \
|
||||||
--hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \
|
--hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \
|
||||||
--hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435
|
--hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
identify==2.6.5 \
|
identify==2.6.12 \
|
||||||
--hash=sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566 \
|
--hash=sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2 \
|
||||||
--hash=sha256:c10b33f250e5bba374fae86fb57f3adcebf1161bce7cdf92031915fd480c13bc
|
--hash=sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
nodeenv==1.9.1 \
|
nodeenv==1.9.1 \
|
||||||
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||||
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
platformdirs==4.3.6 \
|
platformdirs==4.3.8 \
|
||||||
--hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \
|
--hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \
|
||||||
--hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb
|
--hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
pre-commit==4.0.1 \
|
pre-commit==4.2.0 \
|
||||||
--hash=sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2 \
|
--hash=sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146 \
|
||||||
--hash=sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878
|
--hash=sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd
|
||||||
# via -r .github\scripts\requirements_pre_commit.in
|
# via -r .github\scripts\requirements_pre_commit.in
|
||||||
pyyaml==6.0.2 \
|
pyyaml==6.0.2 \
|
||||||
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
|
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
|
||||||
@@ -87,7 +87,7 @@ pyyaml==6.0.2 \
|
|||||||
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
|
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
|
||||||
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
|
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
virtualenv==20.28.1 \
|
virtualenv==20.31.2 \
|
||||||
--hash=sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb \
|
--hash=sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11 \
|
||||||
--hash=sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329
|
--hash=sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
# This file is autogenerated by pip-compile with Python 3.10
|
# This file is autogenerated by pip-compile with Python 3.10
|
||||||
# by the following command:
|
# by the following command:
|
||||||
#
|
#
|
||||||
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_sync_readme.txt' '.github\scripts\requirements_sync_readme.in'
|
# pip-compile --generate-hashes --output-file='.github\scripts\requirements_sync_readme.txt' --strip-extras '.github\scripts\requirements_sync_readme.in'
|
||||||
#
|
#
|
||||||
tomlkit==0.13.2 \
|
tomlkit==0.13.3 \
|
||||||
--hash=sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde \
|
--hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \
|
||||||
--hash=sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79
|
--hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0
|
||||||
# via -r .github\scripts\requirements_sync_readme.in
|
# via -r .github\scripts\requirements_sync_readme.in
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ jobs:
|
|||||||
github.event.comment.user.login == 'sbplat' ||
|
github.event.comment.user.login == 'sbplat' ||
|
||||||
github.event.comment.user.login == 'reecebrowne' ||
|
github.event.comment.user.login == 'reecebrowne' ||
|
||||||
github.event.comment.user.login == 'DarioGii' ||
|
github.event.comment.user.login == 'DarioGii' ||
|
||||||
|
github.event.comment.user.login == 'EthanHealy01' ||
|
||||||
github.event.comment.user.login == 'ConnorYoh'
|
github.event.comment.user.login == 'ConnorYoh'
|
||||||
)
|
)
|
||||||
outputs:
|
outputs:
|
||||||
@@ -42,7 +43,7 @@ jobs:
|
|||||||
enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }}
|
enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -149,7 +150,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
pull-requests: write # Allow writing to pull requests
|
pull-requests: write # Allow writing to pull requests
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
repository-projects: write # Required for enabling automerge
|
repository-projects: write # Required for enabling automerge
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -21,27 +21,31 @@ jobs:
|
|||||||
versionMac: ${{ steps.versionNumberMac.outputs.versionNumberMac }}
|
versionMac: ${{ steps.versionNumberMac.outputs.versionNumberMac }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
# Get version number
|
- name: Set up JDK
|
||||||
|
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '21'
|
||||||
|
|
||||||
|
# ✅ Get version from Gradle
|
||||||
- name: Get version number
|
- name: Get version number
|
||||||
id: versionNumber
|
id: versionNumber
|
||||||
run: |
|
run: |
|
||||||
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
VERSION=$(./gradlew printVersion --quiet | tail -1)
|
||||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# ✅ Get Mac-specific version from Gradle
|
||||||
- name: Get version number mac
|
- name: Get version number mac
|
||||||
id: versionNumberMac
|
id: versionNumberMac
|
||||||
run: |
|
run: |
|
||||||
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
VERSION_MAC=$(./gradlew printMacVersion --quiet | tail -1)
|
||||||
CURRENT_YEAR=$(date +'%Y')
|
echo "versionNumberMac=$VERSION_MAC" >> $GITHUB_OUTPUT
|
||||||
IFS='.' read -r -a VERSION_PARTS <<< "$VERSION"
|
|
||||||
MAC_VERSION="$CURRENT_YEAR.${VERSION_PARTS[1]:-0}.${VERSION_PARTS[2]:-0}"
|
|
||||||
echo "versionNumberMac=$MAC_VERSION" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
build-portable:
|
build-portable:
|
||||||
needs: read_versions
|
needs: read_versions
|
||||||
@@ -56,7 +60,7 @@ jobs:
|
|||||||
file_suffix: ""
|
file_suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -82,7 +86,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir ./binaries
|
mkdir ./binaries
|
||||||
mv ./build/launch4j/Stirling-PDF.exe ./binaries/win-Stirling-PDF-portable-Server${{ matrix.file_suffix }}.exe
|
mv ./build/launch4j/Stirling-PDF.exe ./binaries/win-Stirling-PDF-portable-Server${{ matrix.file_suffix }}.exe
|
||||||
mv ./build/libs/Stirling-PDF-${{ needs.read_versions.outputs.version }}.jar ./binaries/Stirling-PDF${{ matrix.file_suffix }}.jar
|
mv ./stirling-pdf/build/libs/stirling-pdf-${{ needs.read_versions.outputs.version }}.jar ./binaries/Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
@@ -106,7 +110,7 @@ jobs:
|
|||||||
file_suffix: ""
|
file_suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -144,7 +148,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -234,7 +238,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -248,7 +252,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
uses: sigstore/cosign-installer@fb28c2b6339dcd94da6e4cbcbc5e888961f6f8c3 # v3.9.0
|
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1
|
||||||
|
|
||||||
- name: Generate key pair
|
- name: Generate key pair
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
@@ -297,7 +301,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
uses: sigstore/cosign-installer@fb28c2b6339dcd94da6e4cbcbc5e888961f6f8c3 # v3.9.0
|
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.4.1"
|
cosign-release: "v2.4.1"
|
||||||
|
|
||||||
@@ -77,6 +77,7 @@ jobs:
|
|||||||
- name: Generate tags
|
- name: Generate tags
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||||
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
@@ -86,11 +87,11 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
|
||||||
|
|
||||||
- name: Build and push main Dockerfile
|
- name: Build and push main Dockerfile
|
||||||
id: build-push-regular
|
id: build-push-regular
|
||||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||||
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
context: .
|
context: .
|
||||||
@@ -153,7 +154,6 @@ jobs:
|
|||||||
- name: Generate tags fat
|
- name: Generate tags fat
|
||||||
id: meta3
|
id: meta3
|
||||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||||
if: github.ref != 'refs/heads/main'
|
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
@@ -163,11 +163,11 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
|
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: Build and push main Dockerfile fat
|
- name: Build and push main Dockerfile fat
|
||||||
id: build-push-fat
|
id: build-push-fat
|
||||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||||
if: github.ref != 'refs/heads/main'
|
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
context: .
|
context: .
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ jobs:
|
|||||||
file_suffix: ""
|
file_suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ jobs:
|
|||||||
run: ls -R
|
run: ls -R
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@fb28c2b6339dcd94da6e4cbcbc5e888961f6f8c3 # v3.9.0
|
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1
|
||||||
|
|
||||||
- name: Generate key pair
|
- name: Generate key pair
|
||||||
run: cosign generate-key-pair
|
run: cosign generate-key-pair
|
||||||
@@ -161,7 +161,7 @@ jobs:
|
|||||||
file_suffix: ""
|
file_suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -74,6 +74,6 @@ jobs:
|
|||||||
|
|
||||||
# Upload the results to GitHub's code scanning dashboard.
|
# Upload the results to GitHub's code scanning dashboard.
|
||||||
- name: "Upload to code-scanning"
|
- name: "Upload to code-scanning"
|
||||||
uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -57,8 +57,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Run git add
|
- name: Run git add
|
||||||
run: |
|
run: |
|
||||||
git add README.md
|
git add README.md scripts/ignore_translation.toml
|
||||||
git diff --staged --quiet || git commit -m ":memo: Sync README.md" || echo "No changes detected"
|
git diff --staged --quiet || git commit -m ":memo: Sync README.md & scripts/ignore_translation.toml" || echo "No changes detected"
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
+13
-2
@@ -25,7 +25,7 @@ Please make sure your Pull Request adheres to the following guidelines:
|
|||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
|
|
||||||
If you would like to add or modify a translation, please see [How to add new languages to Stirling-PDF](HowToAddNewLanguage.md). Also, please create a Pull Request so others can use it!
|
If you would like to add or modify a translation, please see [How to add new languages to Stirling-PDF](devGuide/HowToAddNewLanguage.md). Also, please create a Pull Request so others can use it!
|
||||||
|
|
||||||
## Docs
|
## Docs
|
||||||
|
|
||||||
@@ -37,7 +37,18 @@ First, make sure you've read the section [Pull Requests](#pull-requests).
|
|||||||
|
|
||||||
If, at any point in time, you have a question, please feel free to ask in the same issue thread or in our [Discord](https://discord.gg/FJUSXUSYec).
|
If, at any point in time, you have a question, please feel free to ask in the same issue thread or in our [Discord](https://discord.gg/FJUSXUSYec).
|
||||||
|
|
||||||
Developers should review our [Developer Guide](DeveloperGuide.md)
|
## Developer Documentation
|
||||||
|
|
||||||
|
For technical guides, setup instructions, and development resources, please see our [Developer Documentation](devGuide/) which includes:
|
||||||
|
|
||||||
|
- [Developer Guide](devGuide/DeveloperGuide.md) - Main setup and architecture guide
|
||||||
|
- [Exception Handling Guide](devGuide/EXCEPTION_HANDLING_GUIDE.md) - Error handling patterns and i18n
|
||||||
|
- [Translation Guide](devGuide/HowToAddNewLanguage.md) - Adding new languages
|
||||||
|
- And more in the [devGuide folder](devGuide/)
|
||||||
|
|
||||||
|
For configuration and usage guides, see:
|
||||||
|
- [Database Guide](DATABASE.md) - Database setup and configuration
|
||||||
|
- [OCR Guide](HowToUseOCR.md) - OCR setup and configuration
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+103
@@ -0,0 +1,103 @@
|
|||||||
|
# Main stage
|
||||||
|
FROM alpine:3.22.0@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715
|
||||||
|
|
||||||
|
# Copy necessary files
|
||||||
|
COPY scripts /scripts
|
||||||
|
COPY pipeline /pipeline
|
||||||
|
COPY stirling-pdf/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
||||||
|
COPY stirling-pdf/build/libs/*.jar app.jar
|
||||||
|
|
||||||
|
ARG VERSION_TAG
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.title="Stirling-PDF"
|
||||||
|
LABEL org.opencontainers.image.description="A powerful locally hosted web-based PDF manipulation tool supporting 50+ operations including merging, splitting, conversion, OCR, watermarking, and more."
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF"
|
||||||
|
LABEL org.opencontainers.image.licenses="MIT"
|
||||||
|
LABEL org.opencontainers.image.vendor="Stirling-Tools"
|
||||||
|
LABEL org.opencontainers.image.url="https://www.stirlingpdf.com"
|
||||||
|
LABEL org.opencontainers.image.documentation="https://docs.stirlingpdf.com"
|
||||||
|
LABEL maintainer="Stirling-Tools"
|
||||||
|
LABEL org.opencontainers.image.authors="Stirling-Tools"
|
||||||
|
LABEL org.opencontainers.image.version="${VERSION_TAG}"
|
||||||
|
LABEL org.opencontainers.image.keywords="PDF, manipulation, merge, split, convert, OCR, watermark"
|
||||||
|
|
||||||
|
# Set Environment Variables
|
||||||
|
ENV DISABLE_ADDITIONAL_FEATURES=true \
|
||||||
|
VERSION_TAG=$VERSION_TAG \
|
||||||
|
JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \
|
||||||
|
JAVA_CUSTOM_OPTS="" \
|
||||||
|
HOME=/home/stirlingpdfuser \
|
||||||
|
PUID=1000 \
|
||||||
|
PGID=1000 \
|
||||||
|
UMASK=022 \
|
||||||
|
PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \
|
||||||
|
UNO_PATH=/usr/lib/libreoffice/program \
|
||||||
|
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \
|
||||||
|
PATH=$PATH:/opt/venv/bin \
|
||||||
|
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
||||||
|
TMPDIR=/tmp/stirling-pdf \
|
||||||
|
TEMP=/tmp/stirling-pdf \
|
||||||
|
TMP=/tmp/stirling-pdf
|
||||||
|
|
||||||
|
|
||||||
|
# JDK for app
|
||||||
|
RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||||
|
echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
||||||
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
||||||
|
apk upgrade --no-cache -a && \
|
||||||
|
apk add --no-cache \
|
||||||
|
ca-certificates \
|
||||||
|
tzdata \
|
||||||
|
tini \
|
||||||
|
bash \
|
||||||
|
curl \
|
||||||
|
shadow \
|
||||||
|
su-exec \
|
||||||
|
openssl \
|
||||||
|
openssl-dev \
|
||||||
|
openjdk21-jre \
|
||||||
|
# Doc conversion
|
||||||
|
gcompat \
|
||||||
|
libc6-compat \
|
||||||
|
libreoffice \
|
||||||
|
# pdftohtml
|
||||||
|
poppler-utils \
|
||||||
|
# OCR MY PDF (unpaper for descew and other advanced features)
|
||||||
|
tesseract-ocr-data-eng \
|
||||||
|
tesseract-ocr-data-chi_sim \
|
||||||
|
tesseract-ocr-data-deu \
|
||||||
|
tesseract-ocr-data-fra \
|
||||||
|
tesseract-ocr-data-por \
|
||||||
|
unpaper \
|
||||||
|
# CV
|
||||||
|
py3-opencv \
|
||||||
|
python3 \
|
||||||
|
ocrmypdf \
|
||||||
|
py3-pip \
|
||||||
|
py3-pillow@testing \
|
||||||
|
py3-pdf2image@testing \
|
||||||
|
# URW Base 35 fonts for better PDF rendering
|
||||||
|
font-urw-base35 && \
|
||||||
|
python3 -m venv /opt/venv && \
|
||||||
|
/opt/venv/bin/pip install --upgrade pip setuptools && \
|
||||||
|
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
|
||||||
|
ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \
|
||||||
|
ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
|
||||||
|
ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \
|
||||||
|
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
||||||
|
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf && \
|
||||||
|
# Configure URW Base 35 fonts
|
||||||
|
ln -s /usr/share/fontconfig/conf.avail/69-urw-*.conf /etc/fonts/conf.d/ && \
|
||||||
|
fc-cache -f -v && \
|
||||||
|
chmod +x /scripts/* && \
|
||||||
|
chmod +x /scripts/init.sh && \
|
||||||
|
# User permissions
|
||||||
|
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||||
|
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \
|
||||||
|
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||||
|
|
||||||
|
EXPOSE 8080/tcp
|
||||||
|
|
||||||
|
# Set user and run command
|
||||||
|
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
||||||
|
CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/tmp/stirling-pdf -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"]
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# dockerfile.dev
|
||||||
|
|
||||||
|
# Basisimage: Gradle mit JDK 17 (Debian-basiert)
|
||||||
|
FROM gradle:8.14-jdk17
|
||||||
|
|
||||||
|
# Als Root-Benutzer arbeiten, um benötigte Pakete zu installieren
|
||||||
|
USER root
|
||||||
|
|
||||||
|
# Set GRADLE_HOME und füge Gradle zum PATH hinzu
|
||||||
|
ENV GRADLE_HOME=/opt/gradle
|
||||||
|
ENV PATH="$GRADLE_HOME/bin:$PATH"
|
||||||
|
|
||||||
|
# Update und Installation zusätzlicher Pakete (Debian/Ubuntu-basiert)
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
sudo \
|
||||||
|
libreoffice \
|
||||||
|
poppler-utils \
|
||||||
|
qpdf \
|
||||||
|
# settings.yml | tessdataDir: /usr/share/tesseract-ocr/5/tessdata
|
||||||
|
tesseract-ocr \
|
||||||
|
tesseract-ocr-eng \
|
||||||
|
fonts-terminus fonts-dejavu fonts-font-awesome fonts-noto fonts-noto-core fonts-noto-cjk fonts-noto-extra fonts-liberation fonts-linuxlibertine fonts-urw-base35 \
|
||||||
|
python3-uno \
|
||||||
|
python3-venv \
|
||||||
|
# ss -tln
|
||||||
|
iproute2 \
|
||||||
|
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Setze die Environment Variable für setuptools
|
||||||
|
ENV SETUPTOOLS_USE_DISTUTILS=local \
|
||||||
|
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
||||||
|
TMPDIR=/tmp/stirling-pdf \
|
||||||
|
TEMP=/tmp/stirling-pdf \
|
||||||
|
TMP=/tmp/stirling-pdf
|
||||||
|
|
||||||
|
# Installation der benötigten Python-Pakete
|
||||||
|
RUN python3 -m venv --system-site-packages /opt/venv \
|
||||||
|
&& . /opt/venv/bin/activate \
|
||||||
|
&& pip install --upgrade pip setuptools \
|
||||||
|
&& pip install --no-cache-dir WeasyPrint pdf2image pillow unoserver opencv-python-headless pre-commit
|
||||||
|
|
||||||
|
# Füge den venv-Pfad zur globalen PATH-Variable hinzu, damit die Tools verfügbar sind
|
||||||
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
|
||||||
|
COPY . /workspace
|
||||||
|
|
||||||
|
RUN mkdir -p /tmp/stirling-pdf \
|
||||||
|
&& fc-cache -f -v \
|
||||||
|
&& adduser --disabled-password --gecos '' devuser \
|
||||||
|
&& chown -R devuser:devuser /home/devuser /workspace /tmp/stirling-pdf
|
||||||
|
RUN echo "devuser ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/devuser \
|
||||||
|
&& chmod 0440 /etc/sudoers.d/devuser
|
||||||
|
|
||||||
|
# Setze das Arbeitsverzeichnis (wird später per Bind-Mount überschrieben)
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
RUN chmod +x /workspace/.devcontainer/git-init.sh
|
||||||
|
RUN sudo chmod +x /workspace/.devcontainer/init-setup.sh
|
||||||
|
|
||||||
|
# Wechsel zum Nicht‑Root Benutzer
|
||||||
|
USER devuser
|
||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
# Build the application
|
||||||
|
FROM gradle:8.14-jdk21 AS build
|
||||||
|
|
||||||
|
COPY build.gradle .
|
||||||
|
COPY settings.gradle .
|
||||||
|
COPY gradlew .
|
||||||
|
COPY gradle gradle/
|
||||||
|
COPY stirling-pdf/build.gradle stirling-pdf/.
|
||||||
|
COPY common/build.gradle common/.
|
||||||
|
COPY proprietary/build.gradle proprietary/.
|
||||||
|
RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
|
||||||
|
|
||||||
|
# Set the working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy the entire project to the working directory
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application with DISABLE_ADDITIONAL_FEATURES=false
|
||||||
|
RUN DISABLE_ADDITIONAL_FEATURES=false \
|
||||||
|
STIRLING_PDF_DESKTOP_UI=false \
|
||||||
|
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||||
|
|
||||||
|
# Main stage
|
||||||
|
FROM alpine:3.22.0@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715
|
||||||
|
|
||||||
|
# Copy necessary files
|
||||||
|
COPY scripts /scripts
|
||||||
|
COPY pipeline /pipeline
|
||||||
|
COPY stirling-pdf/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
||||||
|
COPY --from=build /app/stirling-pdf/build/libs/*.jar app.jar
|
||||||
|
|
||||||
|
ARG VERSION_TAG
|
||||||
|
|
||||||
|
# Set Environment Variables
|
||||||
|
ENV DISABLE_ADDITIONAL_FEATURES=true \
|
||||||
|
VERSION_TAG=$VERSION_TAG \
|
||||||
|
JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \
|
||||||
|
JAVA_CUSTOM_OPTS="" \
|
||||||
|
HOME=/home/stirlingpdfuser \
|
||||||
|
PUID=1000 \
|
||||||
|
PGID=1000 \
|
||||||
|
UMASK=022 \
|
||||||
|
FAT_DOCKER=true \
|
||||||
|
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
|
||||||
|
PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \
|
||||||
|
UNO_PATH=/usr/lib/libreoffice/program \
|
||||||
|
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \
|
||||||
|
PATH=$PATH:/opt/venv/bin \
|
||||||
|
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
||||||
|
TMPDIR=/tmp/stirling-pdf \
|
||||||
|
TEMP=/tmp/stirling-pdf \
|
||||||
|
TMP=/tmp/stirling-pdf
|
||||||
|
|
||||||
|
|
||||||
|
# JDK for app
|
||||||
|
RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||||
|
echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
||||||
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
||||||
|
apk upgrade --no-cache -a && \
|
||||||
|
apk add --no-cache \
|
||||||
|
ca-certificates \
|
||||||
|
tzdata \
|
||||||
|
tini \
|
||||||
|
bash \
|
||||||
|
curl \
|
||||||
|
shadow \
|
||||||
|
su-exec \
|
||||||
|
openssl \
|
||||||
|
openssl-dev \
|
||||||
|
openjdk21-jre \
|
||||||
|
# Doc conversion
|
||||||
|
gcompat \
|
||||||
|
libc6-compat \
|
||||||
|
libreoffice \
|
||||||
|
# pdftohtml
|
||||||
|
poppler-utils \
|
||||||
|
# OCR MY PDF (unpaper for descew and other advanced featues)
|
||||||
|
tesseract-ocr-data-eng \
|
||||||
|
tesseract-ocr-data-chi_sim \
|
||||||
|
tesseract-ocr-data-deu \
|
||||||
|
tesseract-ocr-data-fra \
|
||||||
|
tesseract-ocr-data-por \
|
||||||
|
unpaper \
|
||||||
|
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra font-liberation font-linux-libertine font-urw-base35 \
|
||||||
|
# CV
|
||||||
|
py3-opencv \
|
||||||
|
python3 \
|
||||||
|
ocrmypdf \
|
||||||
|
py3-pip \
|
||||||
|
py3-pillow@testing \
|
||||||
|
py3-pdf2image@testing && \
|
||||||
|
python3 -m venv /opt/venv && \
|
||||||
|
/opt/venv/bin/pip install --upgrade pip setuptools && \
|
||||||
|
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
|
||||||
|
ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \
|
||||||
|
ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
|
||||||
|
ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \
|
||||||
|
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
||||||
|
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf && \
|
||||||
|
# Configure URW Base 35 fonts
|
||||||
|
ln -s /usr/share/fontconfig/conf.avail/69-urw-*.conf /etc/fonts/conf.d/ && \
|
||||||
|
fc-cache -f -v && \
|
||||||
|
chmod +x /scripts/* && \
|
||||||
|
chmod +x /scripts/init.sh && \
|
||||||
|
# User permissions
|
||||||
|
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||||
|
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \
|
||||||
|
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||||
|
|
||||||
|
EXPOSE 8080/tcp
|
||||||
|
# Set user and run command
|
||||||
|
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
||||||
|
CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/tmp/stirling-pdf -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"]
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# use alpine
|
||||||
|
FROM alpine:3.22.0@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715
|
||||||
|
|
||||||
|
ARG VERSION_TAG
|
||||||
|
|
||||||
|
# Set Environment Variables
|
||||||
|
ENV DISABLE_ADDITIONAL_FEATURES=true \
|
||||||
|
HOME=/home/stirlingpdfuser \
|
||||||
|
VERSION_TAG=$VERSION_TAG \
|
||||||
|
JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \
|
||||||
|
JAVA_CUSTOM_OPTS="" \
|
||||||
|
PUID=1000 \
|
||||||
|
PGID=1000 \
|
||||||
|
UMASK=022 \
|
||||||
|
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
||||||
|
TMPDIR=/tmp/stirling-pdf \
|
||||||
|
TEMP=/tmp/stirling-pdf \
|
||||||
|
TMP=/tmp/stirling-pdf
|
||||||
|
|
||||||
|
# Copy necessary files
|
||||||
|
COPY scripts/download-security-jar.sh /scripts/download-security-jar.sh
|
||||||
|
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
|
||||||
|
COPY scripts/installFonts.sh /scripts/installFonts.sh
|
||||||
|
COPY pipeline /pipeline
|
||||||
|
COPY stirling-pdf/build/libs/*.jar app.jar
|
||||||
|
|
||||||
|
# Set up necessary directories and permissions
|
||||||
|
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||||
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
||||||
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
||||||
|
apk upgrade --no-cache -a && \
|
||||||
|
apk add --no-cache \
|
||||||
|
ca-certificates \
|
||||||
|
tzdata \
|
||||||
|
tini \
|
||||||
|
bash \
|
||||||
|
curl \
|
||||||
|
shadow \
|
||||||
|
su-exec \
|
||||||
|
openjdk21-jre && \
|
||||||
|
# User permissions
|
||||||
|
mkdir -p /configs /logs /customFiles /usr/share/fonts/opentype/noto /tmp/stirling-pdf && \
|
||||||
|
chmod +x /scripts/*.sh && \
|
||||||
|
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||||
|
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline /tmp/stirling-pdf && \
|
||||||
|
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
||||||
|
|
||||||
|
EXPOSE 8080/tcp
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
ENTRYPOINT ["tini", "--", "/scripts/init-without-ocr.sh"]
|
||||||
|
CMD ["java", "-Dfile.encoding=UTF-8", "-Djava.io.tmpdir=/tmp/stirling-pdf", "-jar", "/app.jar"]
|
||||||
@@ -29,7 +29,7 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir
|
|||||||
- API for integration with external scripts
|
- API for integration with external scripts
|
||||||
- Optional Login and Authentication support (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/System%20and%20Security) for documentation)
|
- Optional Login and Authentication support (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/System%20and%20Security) for documentation)
|
||||||
- Database Backup and Import (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/DATABASE) for documentation)
|
- Database Backup and Import (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/DATABASE) for documentation)
|
||||||
- Enterprise features like SSO see [here](https://docs.stirlingpdf.com/Enterprise%20Edition)
|
- Enterprise features like SSO (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration) for documentation)
|
||||||
|
|
||||||
## PDF Features
|
## PDF Features
|
||||||
|
|
||||||
@@ -116,47 +116,47 @@ Stirling-PDF currently supports 40 languages!
|
|||||||
|
|
||||||
| Language | Progress |
|
| Language | Progress |
|
||||||
| -------------------------------------------- | -------------------------------------- |
|
| -------------------------------------------- | -------------------------------------- |
|
||||||
| Arabic (العربية) (ar_AR) |  |
|
| Arabic (العربية) (ar_AR) |  |
|
||||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||||
| Basque (Euskara) (eu_ES) |  |
|
| Basque (Euskara) (eu_ES) |  |
|
||||||
| Bulgarian (Български) (bg_BG) |  |
|
| Bulgarian (Български) (bg_BG) |  |
|
||||||
| Catalan (Català) (ca_CA) |  |
|
| Catalan (Català) (ca_CA) |  |
|
||||||
| Croatian (Hrvatski) (hr_HR) |  |
|
| Croatian (Hrvatski) (hr_HR) |  |
|
||||||
| Czech (Česky) (cs_CZ) |  |
|
| Czech (Česky) (cs_CZ) |  |
|
||||||
| Danish (Dansk) (da_DK) |  |
|
| Danish (Dansk) (da_DK) |  |
|
||||||
| Dutch (Nederlands) (nl_NL) |  |
|
| Dutch (Nederlands) (nl_NL) |  |
|
||||||
| English (English) (en_GB) |  |
|
| English (English) (en_GB) |  |
|
||||||
| English (US) (en_US) |  |
|
| English (US) (en_US) |  |
|
||||||
| French (Français) (fr_FR) |  |
|
| French (Français) (fr_FR) |  |
|
||||||
| German (Deutsch) (de_DE) |  |
|
| German (Deutsch) (de_DE) |  |
|
||||||
| Greek (Ελληνικά) (el_GR) |  |
|
| Greek (Ελληνικά) (el_GR) |  |
|
||||||
| Hindi (हिंदी) (hi_IN) |  |
|
| Hindi (हिंदी) (hi_IN) |  |
|
||||||
| Hungarian (Magyar) (hu_HU) |  |
|
| Hungarian (Magyar) (hu_HU) |  |
|
||||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||||
| Irish (Gaeilge) (ga_IE) |  |
|
| Irish (Gaeilge) (ga_IE) |  |
|
||||||
| Italian (Italiano) (it_IT) |  |
|
| Italian (Italiano) (it_IT) |  |
|
||||||
| Japanese (日本語) (ja_JP) |  |
|
| Japanese (日本語) (ja_JP) |  |
|
||||||
| Korean (한국어) (ko_KR) |  |
|
| Korean (한국어) (ko_KR) |  |
|
||||||
| Norwegian (Norsk) (no_NB) |  |
|
| Norwegian (Norsk) (no_NB) |  |
|
||||||
| Persian (فارسی) (fa_IR) |  |
|
| Persian (فارسی) (fa_IR) |  |
|
||||||
| Polish (Polski) (pl_PL) |  |
|
| Polish (Polski) (pl_PL) |  |
|
||||||
| Portuguese (Português) (pt_PT) |  |
|
| Portuguese (Português) (pt_PT) |  |
|
||||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||||
| Romanian (Română) (ro_RO) |  |
|
| Romanian (Română) (ro_RO) |  |
|
||||||
| Russian (Русский) (ru_RU) |  |
|
| Russian (Русский) (ru_RU) |  |
|
||||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||||
| Slovakian (Slovensky) (sk_SK) |  |
|
| Slovakian (Slovensky) (sk_SK) |  |
|
||||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||||
| Spanish (Español) (es_ES) |  |
|
| Spanish (Español) (es_ES) |  |
|
||||||
| Swedish (Svenska) (sv_SE) |  |
|
| Swedish (Svenska) (sv_SE) |  |
|
||||||
| Thai (ไทย) (th_TH) |  |
|
| Thai (ไทย) (th_TH) |  |
|
||||||
| Tibetan (བོད་ཡིག་) (bo_CN) |  |
|
| Tibetan (བོད་ཡིག་) (bo_CN) |  |
|
||||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||||
| Turkish (Türkçe) (tr_TR) |  |
|
| Turkish (Türkçe) (tr_TR) |  |
|
||||||
| Ukrainian (Українська) (uk_UA) |  |
|
| Ukrainian (Українська) (uk_UA) |  |
|
||||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||||
| Malayalam (മലയാളം) (ml_IN) |  |
|
| Malayalam (മലയാളം) (ml_IN) |  |
|
||||||
|
|
||||||
## Stirling PDF Enterprise
|
## Stirling PDF Enterprise
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ Check out our [Enterprise docs](https://docs.stirlingpdf.com/Pro)
|
|||||||
|
|
||||||
Join our community:
|
Join our community:
|
||||||
- [Contribution Guidelines](CONTRIBUTING.md)
|
- [Contribution Guidelines](CONTRIBUTING.md)
|
||||||
- [Translation Guide (How to add custom languages)](HowToAddNewLanguage.md)
|
- [Translation Guide (How to add custom languages)](devGuide/HowToAddNewLanguage.md)
|
||||||
|
- [Developer Guide](devGuide/DeveloperGuide.md)
|
||||||
- [Issue Tracker](https://github.com/Stirling-Tools/Stirling-PDF/issues)
|
- [Issue Tracker](https://github.com/Stirling-Tools/Stirling-PDF/issues)
|
||||||
- [Discord Community](https://discord.gg/HYmhKj45pU)
|
- [Discord Community](https://discord.gg/HYmhKj45pU)
|
||||||
- [Developer Guide](DeveloperGuide.md)
|
|
||||||
|
|||||||
+49
-45
@@ -9,12 +9,14 @@ plugins {
|
|||||||
id "com.diffplug.spotless" version "7.0.4"
|
id "com.diffplug.spotless" version "7.0.4"
|
||||||
id "com.github.jk1.dependency-license-report" version "2.9"
|
id "com.github.jk1.dependency-license-report" version "2.9"
|
||||||
//id "nebula.lint" version "19.0.3"
|
//id "nebula.lint" version "19.0.3"
|
||||||
id "org.panteleyev.jpackageplugin" version "1.6.1"
|
id "org.panteleyev.jpackageplugin" version "1.7.3"
|
||||||
id "org.sonarqube" version "6.2.0.5505"
|
id "org.sonarqube" version "6.2.0.5505"
|
||||||
}
|
}
|
||||||
|
|
||||||
import com.github.jk1.license.render.*
|
import com.github.jk1.license.render.*
|
||||||
import org.gradle.internal.os.OperatingSystem
|
import org.gradle.internal.os.OperatingSystem
|
||||||
|
import org.panteleyev.jpackage.ImageType
|
||||||
|
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.time.Year
|
import java.time.Year
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@ ext {
|
|||||||
bouncycastleVersion = "1.81"
|
bouncycastleVersion = "1.81"
|
||||||
springSecuritySamlVersion = "6.5.1"
|
springSecuritySamlVersion = "6.5.1"
|
||||||
openSamlVersion = "4.3.2"
|
openSamlVersion = "4.3.2"
|
||||||
commonmarkVersion = "0.24.0"
|
commonmarkVersion = "0.25.0"
|
||||||
googleJavaFormatVersion = "1.27.0"
|
googleJavaFormatVersion = "1.27.0"
|
||||||
tempJrePath = null
|
tempJrePath = null
|
||||||
}
|
}
|
||||||
@@ -43,39 +45,19 @@ bootJar {
|
|||||||
enabled = false
|
enabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
// Configure main class for the root project
|
||||||
main {
|
springBoot {
|
||||||
java {
|
mainClass = 'stirling.software.SPDF.SPDFApplication'
|
||||||
if (System.getenv('DOCKER_ENABLE_SECURITY') == 'false' || System.getenv('DISABLE_ADDITIONAL_FEATURES') == 'true'
|
}
|
||||||
|| (project.hasProperty('DISABLE_ADDITIONAL_FEATURES')
|
|
||||||
&& System.getProperty('DISABLE_ADDITIONAL_FEATURES') == 'true')) {
|
|
||||||
exclude 'stirling/software/proprietary/security/**'
|
|
||||||
}
|
|
||||||
if (System.getenv('STIRLING_PDF_DESKTOP_UI') == 'false') {
|
|
||||||
exclude 'stirling/software/SPDF/UI/impl/**'
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
repositories {
|
||||||
}
|
mavenCentral()
|
||||||
|
maven { url = 'https://build.shibboleth.net/maven/releases' }
|
||||||
test {
|
|
||||||
java {
|
|
||||||
if (System.getenv('DOCKER_ENABLE_SECURITY') == 'false' || System.getenv('DISABLE_ADDITIONAL_FEATURES') == 'true'
|
|
||||||
|| (project.hasProperty('DISABLE_ADDITIONAL_FEATURES')
|
|
||||||
&& System.getProperty('DISABLE_ADDITIONAL_FEATURES') == 'true')) {
|
|
||||||
exclude 'stirling/software/proprietary/security/**'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (System.getenv('STIRLING_PDF_DESKTOP_UI') == 'false') {
|
|
||||||
exclude 'stirling/software/SPDF/UI/impl/**'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = 'stirling.software'
|
group = 'stirling.software'
|
||||||
version = '1.0.0'
|
version = '1.0.2'
|
||||||
|
|
||||||
configurations.configureEach {
|
configurations.configureEach {
|
||||||
exclude group: 'commons-logging', module: 'commons-logging'
|
exclude group: 'commons-logging', module: 'commons-logging'
|
||||||
@@ -83,7 +65,6 @@ allprojects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
tasks.register('writeVersion') {
|
tasks.register('writeVersion') {
|
||||||
def propsFile = file("$projectDir/common/src/main/resources/version.properties")
|
def propsFile = file("$projectDir/common/src/main/resources/version.properties")
|
||||||
def propsDir = propsFile.parentFile
|
def propsDir = propsFile.parentFile
|
||||||
@@ -108,6 +89,10 @@ tasks.register('writeVersion') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.named('createExe') {
|
||||||
|
dependsOn(":stirling-pdf:bootJar")
|
||||||
|
}
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
apply plugin: 'java'
|
apply plugin: 'java'
|
||||||
apply plugin: 'java-library'
|
apply plugin: 'java-library'
|
||||||
@@ -120,9 +105,11 @@ subprojects {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
bootJar {
|
if (project.name != "stirling-pdf") {
|
||||||
enabled = false
|
bootJar {
|
||||||
}
|
enabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
@@ -224,6 +211,14 @@ openApi {
|
|||||||
waitTimeInSeconds = 60 // Increase the wait time to 60 seconds
|
waitTimeInSeconds = 60 // Increase the wait time to 60 seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Configure the forked spring boot run task to properly delegate to the stirling-pdf module
|
||||||
|
tasks.named('forkedSpringBootRun') {
|
||||||
|
dependsOn ':stirling-pdf:bootRun'
|
||||||
|
doFirst {
|
||||||
|
println "Delegating forkedSpringBootRun to :stirling-pdf:bootRun"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//0.11.5 to 2024.11.5
|
//0.11.5 to 2024.11.5
|
||||||
static def getMacVersion(String version) {
|
static def getMacVersion(String version) {
|
||||||
def currentYear = Year.now().getValue()
|
def currentYear = Year.now().getValue()
|
||||||
@@ -232,14 +227,15 @@ static def getMacVersion(String version) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jpackage {
|
jpackage {
|
||||||
input = "build/libs"
|
dependsOn(":stirling-pdf:bootJar")
|
||||||
destination = "${projectDir}/build/jpackage"
|
input = layout.projectDirectory.dir("stirling-pdf/build/libs")
|
||||||
|
destination = layout.projectDirectory.dir("build/jpackage")
|
||||||
mainJar = "Stirling-PDF-${project.version}.jar"
|
mainJar = "Stirling-PDF-${project.version}.jar"
|
||||||
appName = "Stirling PDF"
|
appName = "Stirling PDF"
|
||||||
appVersion = project.version
|
appVersion = project.version
|
||||||
vendor = "Stirling PDF Inc"
|
vendor = "Stirling PDF Inc"
|
||||||
appDescription = "Stirling PDF - Your Local PDF Editor"
|
appDescription = "Stirling PDF - Your Local PDF Editor"
|
||||||
icon = "stirling-pdf/src/main/resources/static/favicon.ico"
|
icon = layout.projectDirectory.file("stirling-pdf/src/main/resources/static/favicon.ico")
|
||||||
verbose = true
|
verbose = true
|
||||||
// mainClass = "org.springframework.boot.loader.launch.JarLauncher"
|
// mainClass = "org.springframework.boot.loader.launch.JarLauncher"
|
||||||
|
|
||||||
@@ -274,15 +270,15 @@ jpackage {
|
|||||||
winUpgradeUuid = "2a43ed0c-b8c2-40cf-89e1-751129b87641" // Unique identifier for updates
|
winUpgradeUuid = "2a43ed0c-b8c2-40cf-89e1-751129b87641" // Unique identifier for updates
|
||||||
winHelpUrl = "https://github.com/Stirling-Tools/Stirling-PDF"
|
winHelpUrl = "https://github.com/Stirling-Tools/Stirling-PDF"
|
||||||
winUpdateUrl = "https://github.com/Stirling-Tools/Stirling-PDF/releases"
|
winUpdateUrl = "https://github.com/Stirling-Tools/Stirling-PDF/releases"
|
||||||
type = "exe"
|
type = ImageType.EXE
|
||||||
installDir = "C:/Program Files/Stirling-PDF"
|
installDir = "C:/Program Files/Stirling-PDF"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MacOS-specific configuration
|
// MacOS-specific configuration
|
||||||
mac {
|
mac {
|
||||||
appVersion = getMacVersion(project.version.toString())
|
appVersion = getMacVersion(project.version.toString())
|
||||||
icon = "stirling-pdf/src/main/resources/static/favicon.icns"
|
icon = layout.projectDirectory.file("stirling-pdf/src/main/resources/static/favicon.icns")
|
||||||
type = "dmg"
|
type = ImageType.DMG
|
||||||
macPackageIdentifier = "Stirling PDF"
|
macPackageIdentifier = "Stirling PDF"
|
||||||
macPackageName = "Stirling PDF"
|
macPackageName = "Stirling PDF"
|
||||||
macAppCategory = "public.app-category.productivity"
|
macAppCategory = "public.app-category.productivity"
|
||||||
@@ -303,8 +299,8 @@ jpackage {
|
|||||||
// Linux-specific configuration
|
// Linux-specific configuration
|
||||||
linux {
|
linux {
|
||||||
appVersion = project.version
|
appVersion = project.version
|
||||||
icon = "stirling-pdf/src/main/resources/static/favicon.png"
|
icon = layout.projectDirectory.file("stirling-pdf/src/main/resources/static/favicon.png")
|
||||||
type = "deb" // Can also use "rpm" for Red Hat-based systems
|
type = ImageType.DEB // Can also use "rpm" for Red Hat-based systems
|
||||||
|
|
||||||
// Debian package configuration
|
// Debian package configuration
|
||||||
//linuxPackageName = "stirlingpdf"
|
//linuxPackageName = "stirlingpdf"
|
||||||
@@ -340,7 +336,7 @@ jpackage {
|
|||||||
|
|
||||||
// Add copyright and license information
|
// Add copyright and license information
|
||||||
copyright = "Copyright © 2025 Stirling PDF Inc."
|
copyright = "Copyright © 2025 Stirling PDF Inc."
|
||||||
licenseFile = "LICENSE"
|
licenseFile = layout.projectDirectory.file("LICENSE")
|
||||||
}
|
}
|
||||||
|
|
||||||
//tasks.wrapper {
|
//tasks.wrapper {
|
||||||
@@ -375,7 +371,7 @@ tasks.register('jpackageMacX64') {
|
|||||||
commandLine 'jpackage',
|
commandLine 'jpackage',
|
||||||
'--type', 'dmg',
|
'--type', 'dmg',
|
||||||
'--name', 'Stirling PDF (x86_64)',
|
'--name', 'Stirling PDF (x86_64)',
|
||||||
'--input', 'build/libs',
|
'--input', 'stirling-pdf/build/libs',
|
||||||
'--main-jar', "Stirling-PDF-${project.version}.jar",
|
'--main-jar', "Stirling-PDF-${project.version}.jar",
|
||||||
'--main-class', 'org.springframework.boot.loader.launch.JarLauncher',
|
'--main-class', 'org.springframework.boot.loader.launch.JarLauncher',
|
||||||
'--runtime-image', file(jrePath + "/zulu-17.jre/Contents/Home"),
|
'--runtime-image', file(jrePath + "/zulu-17.jre/Contents/Home"),
|
||||||
@@ -475,7 +471,7 @@ launch4j {
|
|||||||
} else {
|
} else {
|
||||||
headerType = "console"
|
headerType = "console"
|
||||||
}
|
}
|
||||||
jarTask = tasks.bootJar
|
jarTask = project(":stirling-pdf").tasks.bootJar
|
||||||
|
|
||||||
errTitle="Encountered error, do you have Java 21?"
|
errTitle="Encountered error, do you have Java 21?"
|
||||||
downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe"
|
downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe"
|
||||||
@@ -537,6 +533,14 @@ swaggerhubUpload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation project(':stirling-pdf')
|
||||||
|
implementation project(':common')
|
||||||
|
if (System.getenv('DISABLE_ADDITIONAL_FEATURES') != 'true'
|
||||||
|
|| (project.hasProperty('DISABLE_ADDITIONAL_FEATURES')
|
||||||
|
&& System.getProperty('DISABLE_ADDITIONAL_FEATURES') != 'true')) {
|
||||||
|
implementation project(':proprietary')
|
||||||
|
}
|
||||||
|
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.2'
|
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.2'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import java.util.Properties;
|
|||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@@ -21,6 +20,7 @@ import org.springframework.core.env.Environment;
|
|||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.core.io.ResourceLoader;
|
import org.springframework.core.io.ResourceLoader;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -148,23 +148,10 @@ public class AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "activeSecurity")
|
@Bean(name = "activeSecurity")
|
||||||
public boolean activeSecurity() {
|
|
||||||
String disableAdditionalFeatures = env.getProperty("DISABLE_ADDITIONAL_FEATURES");
|
|
||||||
|
|
||||||
if (disableAdditionalFeatures != null) {
|
|
||||||
// DISABLE_ADDITIONAL_FEATURES=true means security OFF, so return false
|
|
||||||
// DISABLE_ADDITIONAL_FEATURES=false means security ON, so return true
|
|
||||||
return !Boolean.parseBoolean(disableAdditionalFeatures);
|
|
||||||
}
|
|
||||||
|
|
||||||
return env.getProperty("DOCKER_ENABLE_SECURITY", Boolean.class, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean(name = "missingActiveSecurity")
|
|
||||||
@ConditionalOnMissingClass(
|
|
||||||
"stirling.software.proprietary.security.configuration.SecurityConfiguration")
|
|
||||||
public boolean missingActiveSecurity() {
|
public boolean missingActiveSecurity() {
|
||||||
return true;
|
return ClassUtils.isPresent(
|
||||||
|
"stirling.software.proprietary.security.configuration.SecurityConfiguration",
|
||||||
|
this.getClass().getClassLoader());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "directoryFilter")
|
@Bean(name = "directoryFilter")
|
||||||
|
|||||||
@@ -545,6 +545,8 @@ public class ApplicationProperties {
|
|||||||
private int calibreSessionLimit;
|
private int calibreSessionLimit;
|
||||||
private int qpdfSessionLimit;
|
private int qpdfSessionLimit;
|
||||||
private int tesseractSessionLimit;
|
private int tesseractSessionLimit;
|
||||||
|
private int ghostscriptSessionLimit;
|
||||||
|
private int ocrMyPdfSessionLimit;
|
||||||
|
|
||||||
public int getQpdfSessionLimit() {
|
public int getQpdfSessionLimit() {
|
||||||
return qpdfSessionLimit > 0 ? qpdfSessionLimit : 2;
|
return qpdfSessionLimit > 0 ? qpdfSessionLimit : 2;
|
||||||
@@ -577,6 +579,14 @@ public class ApplicationProperties {
|
|||||||
public int getCalibreSessionLimit() {
|
public int getCalibreSessionLimit() {
|
||||||
return calibreSessionLimit > 0 ? calibreSessionLimit : 1;
|
return calibreSessionLimit > 0 ? calibreSessionLimit : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getGhostscriptSessionLimit() {
|
||||||
|
return ghostscriptSessionLimit > 0 ? ghostscriptSessionLimit : 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOcrMyPdfSessionLimit() {
|
||||||
|
return ocrMyPdfSessionLimit > 0 ? ocrMyPdfSessionLimit : 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -589,6 +599,8 @@ public class ApplicationProperties {
|
|||||||
private long calibreTimeoutMinutes;
|
private long calibreTimeoutMinutes;
|
||||||
private long tesseractTimeoutMinutes;
|
private long tesseractTimeoutMinutes;
|
||||||
private long qpdfTimeoutMinutes;
|
private long qpdfTimeoutMinutes;
|
||||||
|
private long ghostscriptTimeoutMinutes;
|
||||||
|
private long ocrMyPdfTimeoutMinutes;
|
||||||
|
|
||||||
public long getTesseractTimeoutMinutes() {
|
public long getTesseractTimeoutMinutes() {
|
||||||
return tesseractTimeoutMinutes > 0 ? tesseractTimeoutMinutes : 30;
|
return tesseractTimeoutMinutes > 0 ? tesseractTimeoutMinutes : 30;
|
||||||
@@ -621,6 +633,14 @@ public class ApplicationProperties {
|
|||||||
public long getCalibreTimeoutMinutes() {
|
public long getCalibreTimeoutMinutes() {
|
||||||
return calibreTimeoutMinutes > 0 ? calibreTimeoutMinutes : 30;
|
return calibreTimeoutMinutes > 0 ? calibreTimeoutMinutes : 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getGhostscriptTimeoutMinutes() {
|
||||||
|
return ghostscriptTimeoutMinutes > 0 ? ghostscriptTimeoutMinutes : 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getOcrMyPdfTimeoutMinutes() {
|
||||||
|
return ocrMyPdfTimeoutMinutes > 0 ? ocrMyPdfTimeoutMinutes : 30;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package stirling.software.common.model.job;
|
package stirling.software.common.model.job;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
@@ -26,14 +27,8 @@ public class JobResult {
|
|||||||
/** Error message if the job failed */
|
/** Error message if the job failed */
|
||||||
private String error;
|
private String error;
|
||||||
|
|
||||||
/** The file ID of the result file, if applicable */
|
/** List of result files for jobs that produce files */
|
||||||
private String fileId;
|
private List<ResultFile> resultFiles;
|
||||||
|
|
||||||
/** Original file name, if applicable */
|
|
||||||
private String originalFileName;
|
|
||||||
|
|
||||||
/** MIME type of the result, if applicable */
|
|
||||||
private String contentType;
|
|
||||||
|
|
||||||
/** Time when the job was created */
|
/** Time when the job was created */
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
@@ -64,21 +59,6 @@ public class JobResult {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark this job as complete with a file result
|
|
||||||
*
|
|
||||||
* @param fileId The file ID of the result
|
|
||||||
* @param originalFileName The original file name
|
|
||||||
* @param contentType The content type of the file
|
|
||||||
*/
|
|
||||||
public void completeWithFile(String fileId, String originalFileName, String contentType) {
|
|
||||||
this.complete = true;
|
|
||||||
this.fileId = fileId;
|
|
||||||
this.originalFileName = originalFileName;
|
|
||||||
this.contentType = contentType;
|
|
||||||
this.completedAt = LocalDateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark this job as complete with a general result
|
* Mark this job as complete with a general result
|
||||||
*
|
*
|
||||||
@@ -101,6 +81,67 @@ public class JobResult {
|
|||||||
this.completedAt = LocalDateTime.now();
|
this.completedAt = LocalDateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark this job as complete with multiple file results
|
||||||
|
*
|
||||||
|
* @param resultFiles The list of result files
|
||||||
|
*/
|
||||||
|
public void completeWithFiles(List<ResultFile> resultFiles) {
|
||||||
|
this.complete = true;
|
||||||
|
this.resultFiles = new ArrayList<>(resultFiles);
|
||||||
|
this.completedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark this job as complete with a single file result (convenience method)
|
||||||
|
*
|
||||||
|
* @param fileId The file ID of the result
|
||||||
|
* @param fileName The file name
|
||||||
|
* @param contentType The content type of the file
|
||||||
|
* @param fileSize The size of the file in bytes
|
||||||
|
*/
|
||||||
|
public void completeWithSingleFile(
|
||||||
|
String fileId, String fileName, String contentType, long fileSize) {
|
||||||
|
ResultFile resultFile =
|
||||||
|
ResultFile.builder()
|
||||||
|
.fileId(fileId)
|
||||||
|
.fileName(fileName)
|
||||||
|
.contentType(contentType)
|
||||||
|
.fileSize(fileSize)
|
||||||
|
.build();
|
||||||
|
completeWithFiles(List.of(resultFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this job has file results
|
||||||
|
*
|
||||||
|
* @return true if this job has file results, false otherwise
|
||||||
|
*/
|
||||||
|
public boolean hasFiles() {
|
||||||
|
return resultFiles != null && !resultFiles.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this job has multiple file results
|
||||||
|
*
|
||||||
|
* @return true if this job has multiple file results, false otherwise
|
||||||
|
*/
|
||||||
|
public boolean hasMultipleFiles() {
|
||||||
|
return resultFiles != null && resultFiles.size() > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all result files
|
||||||
|
*
|
||||||
|
* @return List of result files
|
||||||
|
*/
|
||||||
|
public List<ResultFile> getAllResultFiles() {
|
||||||
|
if (resultFiles != null && !resultFiles.isEmpty()) {
|
||||||
|
return Collections.unmodifiableList(resultFiles);
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a note to this job
|
* Add a note to this job
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package stirling.software.common.model.job;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/** Represents a single file result from a job execution */
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ResultFile {
|
||||||
|
|
||||||
|
/** The file ID for accessing the file */
|
||||||
|
private String fileId;
|
||||||
|
|
||||||
|
/** The original file name */
|
||||||
|
private String fileName;
|
||||||
|
|
||||||
|
/** MIME type of the file */
|
||||||
|
private String contentType;
|
||||||
|
|
||||||
|
/** Size of the file in bytes */
|
||||||
|
private long fileSize;
|
||||||
|
}
|
||||||
+46
-8
@@ -24,6 +24,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
|
|
||||||
import stirling.software.common.model.api.PDFFile;
|
import stirling.software.common.model.api.PDFFile;
|
||||||
import stirling.software.common.util.ApplicationContextProvider;
|
import stirling.software.common.util.ApplicationContextProvider;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.TempFileManager;
|
import stirling.software.common.util.TempFileManager;
|
||||||
import stirling.software.common.util.TempFileRegistry;
|
import stirling.software.common.util.TempFileRegistry;
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ public class CustomPDFDocumentFactory {
|
|||||||
*/
|
*/
|
||||||
public PDDocument load(File file, boolean readOnly) throws IOException {
|
public PDDocument load(File file, boolean readOnly) throws IOException {
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
throw new IllegalArgumentException("File cannot be null");
|
throw ExceptionUtils.createNullArgumentException("File");
|
||||||
}
|
}
|
||||||
|
|
||||||
long fileSize = file.length();
|
long fileSize = file.length();
|
||||||
@@ -109,7 +110,7 @@ public class CustomPDFDocumentFactory {
|
|||||||
*/
|
*/
|
||||||
public PDDocument load(Path path, boolean readOnly) throws IOException {
|
public PDDocument load(Path path, boolean readOnly) throws IOException {
|
||||||
if (path == null) {
|
if (path == null) {
|
||||||
throw new IllegalArgumentException("File cannot be null");
|
throw ExceptionUtils.createNullArgumentException("File");
|
||||||
}
|
}
|
||||||
|
|
||||||
long fileSize = Files.size(path);
|
long fileSize = Files.size(path);
|
||||||
@@ -130,7 +131,7 @@ public class CustomPDFDocumentFactory {
|
|||||||
/** Load a PDF from byte array with automatic optimization and read-only option. */
|
/** Load a PDF from byte array with automatic optimization and read-only option. */
|
||||||
public PDDocument load(byte[] input, boolean readOnly) throws IOException {
|
public PDDocument load(byte[] input, boolean readOnly) throws IOException {
|
||||||
if (input == null) {
|
if (input == null) {
|
||||||
throw new IllegalArgumentException("Input bytes cannot be null");
|
throw ExceptionUtils.createNullArgumentException("Input bytes");
|
||||||
}
|
}
|
||||||
|
|
||||||
long dataSize = input.length;
|
long dataSize = input.length;
|
||||||
@@ -151,7 +152,7 @@ public class CustomPDFDocumentFactory {
|
|||||||
/** Load a PDF from InputStream with automatic optimization and read-only option. */
|
/** Load a PDF from InputStream with automatic optimization and read-only option. */
|
||||||
public PDDocument load(InputStream input, boolean readOnly) throws IOException {
|
public PDDocument load(InputStream input, boolean readOnly) throws IOException {
|
||||||
if (input == null) {
|
if (input == null) {
|
||||||
throw new IllegalArgumentException("InputStream cannot be null");
|
throw ExceptionUtils.createNullArgumentException("InputStream");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since we don't know the size upfront, buffer to a temp file
|
// Since we don't know the size upfront, buffer to a temp file
|
||||||
@@ -174,7 +175,7 @@ public class CustomPDFDocumentFactory {
|
|||||||
public PDDocument load(InputStream input, String password, boolean readOnly)
|
public PDDocument load(InputStream input, String password, boolean readOnly)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
if (input == null) {
|
if (input == null) {
|
||||||
throw new IllegalArgumentException("InputStream cannot be null");
|
throw ExceptionUtils.createNullArgumentException("InputStream");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since we don't know the size upfront, buffer to a temp file
|
// Since we don't know the size upfront, buffer to a temp file
|
||||||
@@ -292,9 +293,32 @@ public class CustomPDFDocumentFactory {
|
|||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Unsupported source type: " + source.getClass());
|
throw new IllegalArgumentException("Unsupported source type: " + source.getClass());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configureResourceCacheIfNeeded(document, contentSize);
|
||||||
|
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure resource cache based on content size and memory constraints. Disables resource
|
||||||
|
* cache for large files or when memory is low to prevent OOM errors.
|
||||||
|
*/
|
||||||
|
private void configureResourceCacheIfNeeded(PDDocument document, long contentSize) {
|
||||||
|
if (contentSize > LARGE_FILE_THRESHOLD) {
|
||||||
|
document.setResourceCache(null);
|
||||||
|
} else {
|
||||||
|
// Check current memory status for smaller files
|
||||||
|
long maxMemory = Runtime.getRuntime().maxMemory();
|
||||||
|
long usedMemory =
|
||||||
|
Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
|
||||||
|
double freeMemoryPercent = (double) (maxMemory - usedMemory) / maxMemory * 100;
|
||||||
|
|
||||||
|
if (freeMemoryPercent < MIN_FREE_MEMORY_PERCENTAGE) {
|
||||||
|
document.setResourceCache(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Load a PDF with password protection using adaptive loading strategies */
|
/** Load a PDF with password protection using adaptive loading strategies */
|
||||||
private PDDocument loadAdaptivelyWithPassword(Object source, long contentSize, String password)
|
private PDDocument loadAdaptivelyWithPassword(Object source, long contentSize, String password)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
@@ -313,6 +337,9 @@ public class CustomPDFDocumentFactory {
|
|||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Unsupported source type: " + source.getClass());
|
throw new IllegalArgumentException("Unsupported source type: " + source.getClass());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configureResourceCacheIfNeeded(document, contentSize);
|
||||||
|
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +381,12 @@ public class CustomPDFDocumentFactory {
|
|||||||
|
|
||||||
private PDDocument loadFromFile(File file, long size, StreamCacheCreateFunction cache)
|
private PDDocument loadFromFile(File file, long size, StreamCacheCreateFunction cache)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
return Loader.loadPDF(new DeletingRandomAccessFile(file), "", null, null, cache);
|
try {
|
||||||
|
return Loader.loadPDF(new DeletingRandomAccessFile(file), "", null, null, cache);
|
||||||
|
} catch (IOException e) {
|
||||||
|
ExceptionUtils.logException("PDF loading from file", e);
|
||||||
|
throw ExceptionUtils.handlePdfException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private PDDocument loadFromBytes(byte[] bytes, long size, StreamCacheCreateFunction cache)
|
private PDDocument loadFromBytes(byte[] bytes, long size, StreamCacheCreateFunction cache)
|
||||||
@@ -366,7 +398,13 @@ public class CustomPDFDocumentFactory {
|
|||||||
Files.write(tempFile, bytes);
|
Files.write(tempFile, bytes);
|
||||||
return loadFromFile(tempFile.toFile(), size, cache);
|
return loadFromFile(tempFile.toFile(), size, cache);
|
||||||
}
|
}
|
||||||
return Loader.loadPDF(bytes, "", null, null, cache);
|
|
||||||
|
try {
|
||||||
|
return Loader.loadPDF(bytes, "", null, null, cache);
|
||||||
|
} catch (IOException e) {
|
||||||
|
ExceptionUtils.logException("PDF loading from bytes", e);
|
||||||
|
throw ExceptionUtils.handlePdfException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public PDDocument createNewDocument(MemoryUsageSetting settings) throws IOException {
|
public PDDocument createNewDocument(MemoryUsageSetting settings) throws IOException {
|
||||||
@@ -399,7 +437,7 @@ public class CustomPDFDocumentFactory {
|
|||||||
try {
|
try {
|
||||||
document.setAllSecurityToBeRemoved(true);
|
document.setAllSecurityToBeRemoved(true);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Decryption failed", e);
|
ExceptionUtils.logException("PDF decryption", e);
|
||||||
throw new IOException("PDF decryption failed", e);
|
throw new IOException("PDF decryption failed", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,14 +131,46 @@ public class FileStorage {
|
|||||||
return Files.exists(filePath);
|
return Files.exists(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of a file by its ID without loading the content into memory
|
||||||
|
*
|
||||||
|
* @param fileId The ID of the file
|
||||||
|
* @return The size of the file in bytes
|
||||||
|
* @throws IOException If the file doesn't exist or can't be read
|
||||||
|
*/
|
||||||
|
public long getFileSize(String fileId) throws IOException {
|
||||||
|
Path filePath = getFilePath(fileId);
|
||||||
|
|
||||||
|
if (!Files.exists(filePath)) {
|
||||||
|
throw new IOException("File not found with ID: " + fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Files.size(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the path for a file ID
|
* Get the path for a file ID
|
||||||
*
|
*
|
||||||
* @param fileId The ID of the file
|
* @param fileId The ID of the file
|
||||||
* @return The path to the file
|
* @return The path to the file
|
||||||
|
* @throws IllegalArgumentException if fileId contains path traversal characters or resolves
|
||||||
|
* outside base directory
|
||||||
*/
|
*/
|
||||||
private Path getFilePath(String fileId) {
|
private Path getFilePath(String fileId) {
|
||||||
return Path.of(tempDirPath).resolve(fileId);
|
// Validate fileId to prevent path traversal
|
||||||
|
if (fileId.contains("..") || fileId.contains("/") || fileId.contains("\\")) {
|
||||||
|
throw new IllegalArgumentException("Invalid file ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
Path basePath = Path.of(tempDirPath).normalize().toAbsolutePath();
|
||||||
|
Path resolvedPath = basePath.resolve(fileId).normalize();
|
||||||
|
|
||||||
|
// Ensure resolved path is within the base directory
|
||||||
|
if (!resolvedPath.startsWith(basePath)) {
|
||||||
|
throw new IllegalArgumentException("File ID resolves to an invalid path");
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
package stirling.software.common.service;
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import io.github.pixee.security.ZipSecurity;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import jakarta.annotation.PreDestroy;
|
import jakarta.annotation.PreDestroy;
|
||||||
|
|
||||||
@@ -17,6 +27,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
|
|
||||||
import stirling.software.common.model.job.JobResult;
|
import stirling.software.common.model.job.JobResult;
|
||||||
import stirling.software.common.model.job.JobStats;
|
import stirling.software.common.model.job.JobStats;
|
||||||
|
import stirling.software.common.model.job.ResultFile;
|
||||||
|
|
||||||
/** Manages async tasks and their results */
|
/** Manages async tasks and their results */
|
||||||
@Service
|
@Service
|
||||||
@@ -80,8 +91,53 @@ public class TaskManager {
|
|||||||
public void setFileResult(
|
public void setFileResult(
|
||||||
String jobId, String fileId, String originalFileName, String contentType) {
|
String jobId, String fileId, String originalFileName, String contentType) {
|
||||||
JobResult jobResult = getOrCreateJobResult(jobId);
|
JobResult jobResult = getOrCreateJobResult(jobId);
|
||||||
jobResult.completeWithFile(fileId, originalFileName, contentType);
|
|
||||||
log.debug("Set file result for job ID: {} with file ID: {}", jobId, fileId);
|
// Check if this is a ZIP file that should be extracted
|
||||||
|
if (isZipFile(contentType, originalFileName)) {
|
||||||
|
try {
|
||||||
|
List<ResultFile> extractedFiles =
|
||||||
|
extractZipToIndividualFiles(fileId, originalFileName);
|
||||||
|
if (!extractedFiles.isEmpty()) {
|
||||||
|
jobResult.completeWithFiles(extractedFiles);
|
||||||
|
log.debug(
|
||||||
|
"Set multiple file results for job ID: {} with {} files extracted from ZIP",
|
||||||
|
jobId,
|
||||||
|
extractedFiles.size());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn(
|
||||||
|
"Failed to extract ZIP file for job {}: {}. Falling back to single file result.",
|
||||||
|
jobId,
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle as single file using new ResultFile approach
|
||||||
|
try {
|
||||||
|
long fileSize = fileStorage.getFileSize(fileId);
|
||||||
|
jobResult.completeWithSingleFile(fileId, originalFileName, contentType, fileSize);
|
||||||
|
log.debug("Set single file result for job ID: {} with file ID: {}", jobId, fileId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn(
|
||||||
|
"Failed to get file size for job {}: {}. Using size 0.", jobId, e.getMessage());
|
||||||
|
jobResult.completeWithSingleFile(fileId, originalFileName, contentType, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the result of a task as multiple files
|
||||||
|
*
|
||||||
|
* @param jobId The job ID
|
||||||
|
* @param resultFiles The list of result files
|
||||||
|
*/
|
||||||
|
public void setMultipleFileResults(String jobId, List<ResultFile> resultFiles) {
|
||||||
|
JobResult jobResult = getOrCreateJobResult(jobId);
|
||||||
|
jobResult.completeWithFiles(resultFiles);
|
||||||
|
log.debug(
|
||||||
|
"Set multiple file results for job ID: {} with {} files",
|
||||||
|
jobId,
|
||||||
|
resultFiles.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,7 +160,7 @@ public class TaskManager {
|
|||||||
public void setComplete(String jobId) {
|
public void setComplete(String jobId) {
|
||||||
JobResult jobResult = getOrCreateJobResult(jobId);
|
JobResult jobResult = getOrCreateJobResult(jobId);
|
||||||
if (jobResult.getResult() == null
|
if (jobResult.getResult() == null
|
||||||
&& jobResult.getFileId() == null
|
&& !jobResult.hasFiles()
|
||||||
&& jobResult.getError() == null) {
|
&& jobResult.getError() == null) {
|
||||||
// If no result or error has been set, mark it as complete with an empty result
|
// If no result or error has been set, mark it as complete with an empty result
|
||||||
jobResult.completeWithResult("Task completed successfully");
|
jobResult.completeWithResult("Task completed successfully");
|
||||||
@@ -186,7 +242,7 @@ public class TaskManager {
|
|||||||
failedJobs++;
|
failedJobs++;
|
||||||
} else {
|
} else {
|
||||||
successfulJobs++;
|
successfulJobs++;
|
||||||
if (result.getFileId() != null) {
|
if (result.hasFiles()) {
|
||||||
fileResultJobs++;
|
fileResultJobs++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,17 +306,8 @@ public class TaskManager {
|
|||||||
&& result.getCompletedAt() != null
|
&& result.getCompletedAt() != null
|
||||||
&& result.getCompletedAt().isBefore(expiryThreshold)) {
|
&& result.getCompletedAt().isBefore(expiryThreshold)) {
|
||||||
|
|
||||||
// If the job has a file result, delete the file
|
// Clean up file results
|
||||||
if (result.getFileId() != null) {
|
cleanupJobFiles(result, entry.getKey());
|
||||||
try {
|
|
||||||
fileStorage.deleteFile(result.getFileId());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn(
|
|
||||||
"Failed to delete file for job {}: {}",
|
|
||||||
entry.getKey(),
|
|
||||||
e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the job result
|
// Remove the job result
|
||||||
jobResults.remove(entry.getKey());
|
jobResults.remove(entry.getKey());
|
||||||
@@ -290,4 +337,128 @@ public class TaskManager {
|
|||||||
cleanupExecutor.shutdownNow();
|
cleanupExecutor.shutdownNow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Check if a file is a ZIP file based on content type and filename */
|
||||||
|
private boolean isZipFile(String contentType, String fileName) {
|
||||||
|
if (contentType != null
|
||||||
|
&& (contentType.equals("application/zip")
|
||||||
|
|| contentType.equals("application/x-zip-compressed"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileName != null && fileName.toLowerCase().endsWith(".zip")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extract a ZIP file into individual files and store them */
|
||||||
|
private List<ResultFile> extractZipToIndividualFiles(
|
||||||
|
String zipFileId, String originalZipFileName) throws IOException {
|
||||||
|
List<ResultFile> extractedFiles = new ArrayList<>();
|
||||||
|
|
||||||
|
MultipartFile zipFile = fileStorage.retrieveFile(zipFileId);
|
||||||
|
|
||||||
|
try (ZipInputStream zipIn =
|
||||||
|
ZipSecurity.createHardenedInputStream(new ByteArrayInputStream(zipFile.getBytes()))) {
|
||||||
|
ZipEntry entry;
|
||||||
|
while ((entry = zipIn.getNextEntry()) != null) {
|
||||||
|
if (!entry.isDirectory()) {
|
||||||
|
// Use buffered reading for memory safety
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = zipIn.read(buffer)) != -1) {
|
||||||
|
out.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
byte[] fileContent = out.toByteArray();
|
||||||
|
|
||||||
|
String contentType = determineContentType(entry.getName());
|
||||||
|
String individualFileId = fileStorage.storeBytes(fileContent, entry.getName());
|
||||||
|
|
||||||
|
ResultFile resultFile =
|
||||||
|
ResultFile.builder()
|
||||||
|
.fileId(individualFileId)
|
||||||
|
.fileName(entry.getName())
|
||||||
|
.contentType(contentType)
|
||||||
|
.fileSize(fileContent.length)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
extractedFiles.add(resultFile);
|
||||||
|
log.debug(
|
||||||
|
"Extracted file: {} (size: {} bytes)",
|
||||||
|
entry.getName(),
|
||||||
|
fileContent.length);
|
||||||
|
}
|
||||||
|
zipIn.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the original ZIP file after extraction
|
||||||
|
try {
|
||||||
|
fileStorage.deleteFile(zipFileId);
|
||||||
|
log.debug("Cleaned up original ZIP file: {}", zipFileId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to clean up original ZIP file {}: {}", zipFileId, e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Determine content type based on file extension */
|
||||||
|
private String determineContentType(String fileName) {
|
||||||
|
if (fileName == null) {
|
||||||
|
return MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
String lowerName = fileName.toLowerCase();
|
||||||
|
if (lowerName.endsWith(".pdf")) {
|
||||||
|
return MediaType.APPLICATION_PDF_VALUE;
|
||||||
|
} else if (lowerName.endsWith(".txt")) {
|
||||||
|
return MediaType.TEXT_PLAIN_VALUE;
|
||||||
|
} else if (lowerName.endsWith(".json")) {
|
||||||
|
return MediaType.APPLICATION_JSON_VALUE;
|
||||||
|
} else if (lowerName.endsWith(".xml")) {
|
||||||
|
return MediaType.APPLICATION_XML_VALUE;
|
||||||
|
} else if (lowerName.endsWith(".jpg") || lowerName.endsWith(".jpeg")) {
|
||||||
|
return MediaType.IMAGE_JPEG_VALUE;
|
||||||
|
} else if (lowerName.endsWith(".png")) {
|
||||||
|
return MediaType.IMAGE_PNG_VALUE;
|
||||||
|
} else {
|
||||||
|
return MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clean up files associated with a job result */
|
||||||
|
private void cleanupJobFiles(JobResult result, String jobId) {
|
||||||
|
// Clean up all result files
|
||||||
|
if (result.hasFiles()) {
|
||||||
|
for (ResultFile resultFile : result.getAllResultFiles()) {
|
||||||
|
try {
|
||||||
|
fileStorage.deleteFile(resultFile.getFileId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn(
|
||||||
|
"Failed to delete file {} for job {}: {}",
|
||||||
|
resultFile.getFileId(),
|
||||||
|
jobId,
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Find the ResultFile metadata for a given file ID by searching through all job results */
|
||||||
|
public ResultFile findResultFileByFileId(String fileId) {
|
||||||
|
for (JobResult jobResult : jobResults.values()) {
|
||||||
|
if (jobResult.hasFiles()) {
|
||||||
|
for (ResultFile resultFile : jobResult.getAllResultFiles()) {
|
||||||
|
if (fileId.equals(resultFile.getFileId())) {
|
||||||
|
return resultFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,11 +154,15 @@ public class TempFileCleanupService {
|
|||||||
boolean containerMode = isContainerMode();
|
boolean containerMode = isContainerMode();
|
||||||
int unregisteredDeletedCount = cleanupUnregisteredFiles(containerMode, true, maxAgeMillis);
|
int unregisteredDeletedCount = cleanupUnregisteredFiles(containerMode, true, maxAgeMillis);
|
||||||
|
|
||||||
log.info(
|
if (registeredDeletedCount > 0
|
||||||
"Scheduled cleanup complete. Deleted {} registered files, {} unregistered files, {} directories",
|
|| unregisteredDeletedCount > 0
|
||||||
registeredDeletedCount,
|
|| directoriesDeletedCount > 0) {
|
||||||
unregisteredDeletedCount,
|
log.info(
|
||||||
directoriesDeletedCount);
|
"Scheduled cleanup complete. Deleted {} registered files, {} unregistered files, {} directories",
|
||||||
|
registeredDeletedCount,
|
||||||
|
unregisteredDeletedCount,
|
||||||
|
directoriesDeletedCount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,7 +170,6 @@ public class TempFileCleanupService {
|
|||||||
* important in Docker environments where temp files persist between container restarts.
|
* important in Docker environments where temp files persist between container restarts.
|
||||||
*/
|
*/
|
||||||
private void runStartupCleanup() {
|
private void runStartupCleanup() {
|
||||||
log.info("Running startup temporary file cleanup");
|
|
||||||
boolean containerMode = isContainerMode();
|
boolean containerMode = isContainerMode();
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
@@ -178,7 +181,6 @@ public class TempFileCleanupService {
|
|||||||
long maxAgeMillis = containerMode ? 0 : 24 * 60 * 60 * 1000; // 0 or 24 hours
|
long maxAgeMillis = containerMode ? 0 : 24 * 60 * 60 * 1000; // 0 or 24 hours
|
||||||
|
|
||||||
int totalDeletedCount = cleanupUnregisteredFiles(containerMode, false, maxAgeMillis);
|
int totalDeletedCount = cleanupUnregisteredFiles(containerMode, false, maxAgeMillis);
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
"Startup cleanup complete. Deleted {} temporary files/directories",
|
"Startup cleanup complete. Deleted {} temporary files/directories",
|
||||||
totalDeletedCount);
|
totalDeletedCount);
|
||||||
@@ -225,7 +227,7 @@ public class TempFileCleanupService {
|
|||||||
tempDir -> {
|
tempDir -> {
|
||||||
try {
|
try {
|
||||||
String phase = isScheduled ? "scheduled" : "startup";
|
String phase = isScheduled ? "scheduled" : "startup";
|
||||||
log.info(
|
log.debug(
|
||||||
"Scanning directory for {} cleanup: {}",
|
"Scanning directory for {} cleanup: {}",
|
||||||
phase,
|
phase,
|
||||||
tempDir);
|
tempDir);
|
||||||
|
|||||||
@@ -0,0 +1,327 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for handling exceptions with internationalized error messages. Provides consistent
|
||||||
|
* error handling and user-friendly messages across the application.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class ExceptionUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an IOException with internationalized message for PDF corruption.
|
||||||
|
*
|
||||||
|
* @param cause the original exception
|
||||||
|
* @return IOException with user-friendly message
|
||||||
|
*/
|
||||||
|
public static IOException createPdfCorruptedException(Exception cause) {
|
||||||
|
return createPdfCorruptedException(null, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an IOException with internationalized message for PDF corruption with context.
|
||||||
|
*
|
||||||
|
* @param context additional context (e.g., "during merge", "during image extraction")
|
||||||
|
* @param cause the original exception
|
||||||
|
* @return IOException with user-friendly message
|
||||||
|
*/
|
||||||
|
public static IOException createPdfCorruptedException(String context, Exception cause) {
|
||||||
|
String message;
|
||||||
|
if (context != null && !context.isEmpty()) {
|
||||||
|
message =
|
||||||
|
String.format(
|
||||||
|
"Error %s: PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.",
|
||||||
|
context);
|
||||||
|
} else {
|
||||||
|
message =
|
||||||
|
"PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.";
|
||||||
|
}
|
||||||
|
return new IOException(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an IOException with internationalized message for multiple corrupted PDFs.
|
||||||
|
*
|
||||||
|
* @param cause the original exception
|
||||||
|
* @return IOException with user-friendly message
|
||||||
|
*/
|
||||||
|
public static IOException createMultiplePdfCorruptedException(Exception cause) {
|
||||||
|
String message =
|
||||||
|
"One or more PDF files appear to be corrupted or damaged. Please try using the 'Repair PDF' feature on each file first before attempting to merge them.";
|
||||||
|
return new IOException(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an IOException with internationalized message for PDF encryption issues.
|
||||||
|
*
|
||||||
|
* @param cause the original exception
|
||||||
|
* @return IOException with user-friendly message
|
||||||
|
*/
|
||||||
|
public static IOException createPdfEncryptionException(Exception cause) {
|
||||||
|
String message =
|
||||||
|
"The PDF appears to have corrupted encryption data. This can happen when the PDF was created with incompatible encryption methods. Please try using the 'Repair PDF' feature first, or contact the document creator for a new copy.";
|
||||||
|
return new IOException(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an IOException with internationalized message for PDF password issues.
|
||||||
|
*
|
||||||
|
* @param cause the original exception
|
||||||
|
* @return IOException with user-friendly message
|
||||||
|
*/
|
||||||
|
public static IOException createPdfPasswordException(Exception cause) {
|
||||||
|
String message =
|
||||||
|
"The PDF Document is passworded and either the password was not provided or was incorrect";
|
||||||
|
return new IOException(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an IOException with internationalized message for file processing errors.
|
||||||
|
*
|
||||||
|
* @param operation the operation being performed (e.g., "merge", "split", "convert")
|
||||||
|
* @param cause the original exception
|
||||||
|
* @return IOException with user-friendly message
|
||||||
|
*/
|
||||||
|
public static IOException createFileProcessingException(String operation, Exception cause) {
|
||||||
|
String message =
|
||||||
|
String.format(
|
||||||
|
"An error occurred while processing the file during %s operation: %s",
|
||||||
|
operation, cause.getMessage());
|
||||||
|
return new IOException(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a generic IOException with internationalized message.
|
||||||
|
*
|
||||||
|
* @param messageKey the i18n message key
|
||||||
|
* @param defaultMessage the default message if i18n is not available
|
||||||
|
* @param cause the original exception
|
||||||
|
* @param args optional arguments for the message
|
||||||
|
* @return IOException with user-friendly message
|
||||||
|
*/
|
||||||
|
public static IOException createIOException(
|
||||||
|
String messageKey, String defaultMessage, Exception cause, Object... args) {
|
||||||
|
String message = MessageFormat.format(defaultMessage, args);
|
||||||
|
return new IOException(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a generic RuntimeException with internationalized message.
|
||||||
|
*
|
||||||
|
* @param messageKey the i18n message key
|
||||||
|
* @param defaultMessage the default message if i18n is not available
|
||||||
|
* @param cause the original exception
|
||||||
|
* @param args optional arguments for the message
|
||||||
|
* @return RuntimeException with user-friendly message
|
||||||
|
*/
|
||||||
|
public static RuntimeException createRuntimeException(
|
||||||
|
String messageKey, String defaultMessage, Exception cause, Object... args) {
|
||||||
|
String message = MessageFormat.format(defaultMessage, args);
|
||||||
|
return new RuntimeException(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an IllegalArgumentException with internationalized message.
|
||||||
|
*
|
||||||
|
* @param messageKey the i18n message key
|
||||||
|
* @param defaultMessage the default message if i18n is not available
|
||||||
|
* @param args optional arguments for the message
|
||||||
|
* @return IllegalArgumentException with user-friendly message
|
||||||
|
*/
|
||||||
|
public static IllegalArgumentException createIllegalArgumentException(
|
||||||
|
String messageKey, String defaultMessage, Object... args) {
|
||||||
|
String message = MessageFormat.format(defaultMessage, args);
|
||||||
|
return new IllegalArgumentException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create file validation exceptions. */
|
||||||
|
public static IllegalArgumentException createHtmlFileRequiredException() {
|
||||||
|
return createIllegalArgumentException(
|
||||||
|
"error.fileFormatRequired", "File must be in {0} format", "HTML or ZIP");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IllegalArgumentException createPdfFileRequiredException() {
|
||||||
|
return createIllegalArgumentException(
|
||||||
|
"error.fileFormatRequired", "File must be in {0} format", "PDF");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IllegalArgumentException createInvalidPageSizeException(String size) {
|
||||||
|
return createIllegalArgumentException(
|
||||||
|
"error.invalidFormat", "Invalid {0} format: {1}", "page size", size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create OCR-related exceptions. */
|
||||||
|
public static IOException createOcrLanguageRequiredException() {
|
||||||
|
return createIOException(
|
||||||
|
"error.optionsNotSpecified", "{0} options are not specified", null, "OCR language");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IOException createOcrInvalidLanguagesException() {
|
||||||
|
return createIOException(
|
||||||
|
"error.invalidFormat",
|
||||||
|
"Invalid {0} format: {1}",
|
||||||
|
null,
|
||||||
|
"OCR languages",
|
||||||
|
"none of the selected languages are valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IOException createOcrToolsUnavailableException() {
|
||||||
|
return createIOException(
|
||||||
|
"error.toolNotInstalled", "{0} is not installed", null, "OCR tools");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create system requirement exceptions. */
|
||||||
|
public static IOException createPythonRequiredForWebpException() {
|
||||||
|
return createIOException(
|
||||||
|
"error.toolRequired", "{0} is required for {1}", null, "Python", "WebP conversion");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create file operation exceptions. */
|
||||||
|
public static IOException createFileNotFoundException(String fileId) {
|
||||||
|
return createIOException("error.fileNotFound", "File not found with ID: {0}", null, fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RuntimeException createPdfaConversionFailedException() {
|
||||||
|
return createRuntimeException(
|
||||||
|
"error.conversionFailed", "{0} conversion failed", null, "PDF/A");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IllegalArgumentException createInvalidComparatorException() {
|
||||||
|
return createIllegalArgumentException(
|
||||||
|
"error.invalidFormat",
|
||||||
|
"Invalid {0} format: {1}",
|
||||||
|
"comparator",
|
||||||
|
"only 'greater', 'equal', and 'less' are supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create compression-related exceptions. */
|
||||||
|
public static RuntimeException createMd5AlgorithmException(Exception cause) {
|
||||||
|
return createRuntimeException(
|
||||||
|
"error.algorithmNotAvailable", "{0} algorithm not available", cause, "MD5");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IllegalArgumentException createCompressionOptionsException() {
|
||||||
|
return createIllegalArgumentException(
|
||||||
|
"error.optionsNotSpecified",
|
||||||
|
"{0} options are not specified",
|
||||||
|
"compression (expected output size and optimize level)");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IOException createGhostscriptCompressionException() {
|
||||||
|
return createIOException(
|
||||||
|
"error.commandFailed", "{0} command failed", null, "Ghostscript compression");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IOException createGhostscriptCompressionException(Exception cause) {
|
||||||
|
return createIOException(
|
||||||
|
"error.commandFailed", "{0} command failed", cause, "Ghostscript compression");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IOException createQpdfCompressionException(Exception cause) {
|
||||||
|
return createIOException("error.commandFailed", "{0} command failed", cause, "QPDF");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an exception indicates a corrupted PDF and wrap it with appropriate message.
|
||||||
|
*
|
||||||
|
* @param e the exception to check
|
||||||
|
* @return the original exception if not PDF corruption, or a new IOException with user-friendly
|
||||||
|
* message
|
||||||
|
*/
|
||||||
|
public static IOException handlePdfException(IOException e) {
|
||||||
|
return handlePdfException(e, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an exception indicates a corrupted PDF and wrap it with appropriate message.
|
||||||
|
*
|
||||||
|
* @param e the exception to check
|
||||||
|
* @param context additional context for the error
|
||||||
|
* @return the original exception if not PDF corruption, or a new IOException with user-friendly
|
||||||
|
* message
|
||||||
|
*/
|
||||||
|
public static IOException handlePdfException(IOException e, String context) {
|
||||||
|
if (PdfErrorUtils.isCorruptedPdfError(e)) {
|
||||||
|
return createPdfCorruptedException(context, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEncryptionError(e)) {
|
||||||
|
return createPdfEncryptionException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPasswordError(e)) {
|
||||||
|
return createPdfPasswordException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return e; // Return original exception if no specific handling needed
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an exception indicates a PDF encryption/decryption error.
|
||||||
|
*
|
||||||
|
* @param e the exception to check
|
||||||
|
* @return true if it's an encryption error, false otherwise
|
||||||
|
*/
|
||||||
|
public static boolean isEncryptionError(IOException e) {
|
||||||
|
String message = e.getMessage();
|
||||||
|
if (message == null) return false;
|
||||||
|
|
||||||
|
return message.contains("BadPaddingException")
|
||||||
|
|| message.contains("Given final block not properly padded")
|
||||||
|
|| message.contains("AES initialization vector not fully read")
|
||||||
|
|| message.contains("Failed to decrypt");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an exception indicates a PDF password error.
|
||||||
|
*
|
||||||
|
* @param e the exception to check
|
||||||
|
* @return true if it's a password error, false otherwise
|
||||||
|
*/
|
||||||
|
public static boolean isPasswordError(IOException e) {
|
||||||
|
String message = e.getMessage();
|
||||||
|
if (message == null) return false;
|
||||||
|
|
||||||
|
return message.contains("password is incorrect")
|
||||||
|
|| message.contains("Password is not provided")
|
||||||
|
|| message.contains("PDF contains an encryption dictionary");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log an exception with appropriate level based on its type.
|
||||||
|
*
|
||||||
|
* @param operation the operation being performed
|
||||||
|
* @param e the exception that occurred
|
||||||
|
*/
|
||||||
|
public static void logException(String operation, Exception e) {
|
||||||
|
if (PdfErrorUtils.isCorruptedPdfError(e)) {
|
||||||
|
log.warn("PDF corruption detected during {}: {}", operation, e.getMessage());
|
||||||
|
} else if (e instanceof IOException
|
||||||
|
&& (isEncryptionError((IOException) e) || isPasswordError((IOException) e))) {
|
||||||
|
log.info("PDF security issue during {}: {}", operation, e.getMessage());
|
||||||
|
} else {
|
||||||
|
log.error("Unexpected error during {}", operation, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create common validation exceptions. */
|
||||||
|
public static IllegalArgumentException createInvalidArgumentException(String argumentName) {
|
||||||
|
return createIllegalArgumentException(
|
||||||
|
"error.invalidArgument", "Invalid argument: {0}", argumentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IllegalArgumentException createInvalidArgumentException(
|
||||||
|
String argumentName, String value) {
|
||||||
|
return createIllegalArgumentException(
|
||||||
|
"error.invalidFormat", "Invalid {0} format: {1}", argumentName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IllegalArgumentException createNullArgumentException(String argumentName) {
|
||||||
|
return createIllegalArgumentException(
|
||||||
|
"error.argumentRequired", "{0} must not be null", argumentName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,21 +32,23 @@ public class FileToPdf {
|
|||||||
|
|
||||||
try (TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf")) {
|
try (TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf")) {
|
||||||
try (TempFile tempInputFile =
|
try (TempFile tempInputFile =
|
||||||
new TempFile(tempFileManager, fileName.endsWith(".html") ? ".html" : ".zip")) {
|
new TempFile(
|
||||||
|
tempFileManager,
|
||||||
|
fileName.toLowerCase().endsWith(".html") ? ".html" : ".zip")) {
|
||||||
|
|
||||||
if (fileName.endsWith(".html")) {
|
if (fileName.toLowerCase().endsWith(".html")) {
|
||||||
String sanitizedHtml =
|
String sanitizedHtml =
|
||||||
sanitizeHtmlContent(
|
sanitizeHtmlContent(
|
||||||
new String(fileBytes, StandardCharsets.UTF_8), disableSanitize);
|
new String(fileBytes, StandardCharsets.UTF_8), disableSanitize);
|
||||||
Files.write(
|
Files.write(
|
||||||
tempInputFile.getPath(),
|
tempInputFile.getPath(),
|
||||||
sanitizedHtml.getBytes(StandardCharsets.UTF_8));
|
sanitizedHtml.getBytes(StandardCharsets.UTF_8));
|
||||||
} else if (fileName.endsWith(".zip")) {
|
} else if (fileName.toLowerCase().endsWith(".zip")) {
|
||||||
Files.write(tempInputFile.getPath(), fileBytes);
|
Files.write(tempInputFile.getPath(), fileBytes);
|
||||||
sanitizeHtmlFilesInZip(
|
sanitizeHtmlFilesInZip(
|
||||||
tempInputFile.getPath(), disableSanitize, tempFileManager);
|
tempInputFile.getPath(), disableSanitize, tempFileManager);
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Unsupported file format: " + fileName);
|
throw ExceptionUtils.createHtmlFileRequiredException();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/** Utility class for detecting and handling PDF-related errors. */
|
||||||
|
public class PdfErrorUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an IOException indicates a corrupted PDF file.
|
||||||
|
*
|
||||||
|
* @param e the IOException to check
|
||||||
|
* @return true if the error indicates PDF corruption, false otherwise
|
||||||
|
*/
|
||||||
|
public static boolean isCorruptedPdfError(IOException e) {
|
||||||
|
return isCorruptedPdfError(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if any Exception indicates a corrupted PDF file.
|
||||||
|
*
|
||||||
|
* @param e the Exception to check
|
||||||
|
* @return true if the error indicates PDF corruption, false otherwise
|
||||||
|
*/
|
||||||
|
public static boolean isCorruptedPdfError(Exception e) {
|
||||||
|
return isCorruptedPdfError(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an error message indicates a corrupted PDF file.
|
||||||
|
*
|
||||||
|
* @param message the error message to check
|
||||||
|
* @return true if the message indicates PDF corruption, false otherwise
|
||||||
|
*/
|
||||||
|
private static boolean isCorruptedPdfError(String message) {
|
||||||
|
if (message == null) return false;
|
||||||
|
|
||||||
|
// Check for common corruption indicators
|
||||||
|
return message.contains("Missing root object specification")
|
||||||
|
|| message.contains("Header doesn't contain versioninfo")
|
||||||
|
|| message.contains("Expected trailer")
|
||||||
|
|| message.contains("Invalid PDF")
|
||||||
|
|| message.contains("Corrupted")
|
||||||
|
|| message.contains("damaged")
|
||||||
|
|| message.contains("Unknown dir object")
|
||||||
|
|| message.contains("Can't dereference COSObject")
|
||||||
|
|| message.contains("parseCOSString string should start with")
|
||||||
|
|| message.contains("ICCBased colorspace array must have a stream")
|
||||||
|
|| message.contains("1-based index not found")
|
||||||
|
|| message.contains("Invalid dictionary, found:")
|
||||||
|
|| message.contains("AES initialization vector not fully read")
|
||||||
|
|| message.contains("BadPaddingException")
|
||||||
|
|| message.contains("Given final block not properly padded")
|
||||||
|
|| message.contains("End-of-File, expected line");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,26 +42,34 @@ public class PdfUtils {
|
|||||||
|
|
||||||
public static PDRectangle textToPageSize(String size) {
|
public static PDRectangle textToPageSize(String size) {
|
||||||
switch (size.toUpperCase()) {
|
switch (size.toUpperCase()) {
|
||||||
case "A0":
|
case "A0" -> {
|
||||||
return PDRectangle.A0;
|
return PDRectangle.A0;
|
||||||
case "A1":
|
}
|
||||||
|
case "A1" -> {
|
||||||
return PDRectangle.A1;
|
return PDRectangle.A1;
|
||||||
case "A2":
|
}
|
||||||
|
case "A2" -> {
|
||||||
return PDRectangle.A2;
|
return PDRectangle.A2;
|
||||||
case "A3":
|
}
|
||||||
|
case "A3" -> {
|
||||||
return PDRectangle.A3;
|
return PDRectangle.A3;
|
||||||
case "A4":
|
}
|
||||||
|
case "A4" -> {
|
||||||
return PDRectangle.A4;
|
return PDRectangle.A4;
|
||||||
case "A5":
|
}
|
||||||
|
case "A5" -> {
|
||||||
return PDRectangle.A5;
|
return PDRectangle.A5;
|
||||||
case "A6":
|
}
|
||||||
|
case "A6" -> {
|
||||||
return PDRectangle.A6;
|
return PDRectangle.A6;
|
||||||
case "LETTER":
|
}
|
||||||
|
case "LETTER" -> {
|
||||||
return PDRectangle.LETTER;
|
return PDRectangle.LETTER;
|
||||||
case "LEGAL":
|
}
|
||||||
|
case "LEGAL" -> {
|
||||||
return PDRectangle.LEGAL;
|
return PDRectangle.LEGAL;
|
||||||
default:
|
}
|
||||||
throw new IllegalArgumentException("Invalid standard page size: " + size);
|
default -> throw ExceptionUtils.createInvalidPageSizeException(size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +143,17 @@ public class PdfUtils {
|
|||||||
int DPI,
|
int DPI,
|
||||||
String filename)
|
String filename)
|
||||||
throws IOException, Exception {
|
throws IOException, Exception {
|
||||||
|
|
||||||
|
// Validate and limit DPI to prevent excessive memory usage
|
||||||
|
final int MAX_SAFE_DPI = 500; // Maximum safe DPI to prevent memory issues
|
||||||
|
if (DPI > MAX_SAFE_DPI) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.dpiExceedsLimit",
|
||||||
|
"DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause memory issues and crashes. Please use a lower DPI value.",
|
||||||
|
DPI,
|
||||||
|
MAX_SAFE_DPI);
|
||||||
|
}
|
||||||
|
|
||||||
try (PDDocument document = pdfDocumentFactory.load(inputStream)) {
|
try (PDDocument document = pdfDocumentFactory.load(inputStream)) {
|
||||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
pdfRenderer.setSubsamplingAllowed(true);
|
pdfRenderer.setSubsamplingAllowed(true);
|
||||||
@@ -158,7 +177,21 @@ public class PdfUtils {
|
|||||||
writer.prepareWriteSequence(null);
|
writer.prepareWriteSequence(null);
|
||||||
|
|
||||||
for (int i = 0; i < pageCount; ++i) {
|
for (int i = 0; i < pageCount; ++i) {
|
||||||
BufferedImage image = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
BufferedImage image;
|
||||||
|
try {
|
||||||
|
image = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
if (e.getMessage() != null
|
||||||
|
&& e.getMessage()
|
||||||
|
.contains("Maximum size of image exceeded")) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.pageTooBigForDpi",
|
||||||
|
"PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).",
|
||||||
|
i + 1,
|
||||||
|
DPI);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
writer.writeToSequence(new IIOImage(image, null, null), param);
|
writer.writeToSequence(new IIOImage(image, null, null), param);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +223,20 @@ public class PdfUtils {
|
|||||||
PdfImageDimensionValue dimension = pageSizes.get(settings);
|
PdfImageDimensionValue dimension = pageSizes.get(settings);
|
||||||
if (dimension == null) {
|
if (dimension == null) {
|
||||||
// Render the image to get the dimensions
|
// Render the image to get the dimensions
|
||||||
pdfSizeImage = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
try {
|
||||||
|
pdfSizeImage = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
if (e.getMessage() != null
|
||||||
|
&& e.getMessage()
|
||||||
|
.contains("Maximum size of image exceeded")) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.pageTooBigExceedsArray",
|
||||||
|
"PDF page {0} is too large to render at {1} DPI. The resulting image would exceed Java's maximum array size. Please try a lower DPI value (recommended: 150 or less).",
|
||||||
|
i + 1,
|
||||||
|
DPI);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
pdfSizeImageIndex = i;
|
pdfSizeImageIndex = i;
|
||||||
dimension =
|
dimension =
|
||||||
new PdfImageDimensionValue(
|
new PdfImageDimensionValue(
|
||||||
@@ -218,7 +264,20 @@ public class PdfUtils {
|
|||||||
if (firstImageAlreadyRendered && i == 0) {
|
if (firstImageAlreadyRendered && i == 0) {
|
||||||
pageImage = pdfSizeImage;
|
pageImage = pdfSizeImage;
|
||||||
} else {
|
} else {
|
||||||
pageImage = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
try {
|
||||||
|
pageImage = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
if (e.getMessage() != null
|
||||||
|
&& e.getMessage()
|
||||||
|
.contains("Maximum size of image exceeded")) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.pageTooBigForDpi",
|
||||||
|
"PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).",
|
||||||
|
i + 1,
|
||||||
|
DPI);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the x-coordinate to center the image
|
// Calculate the x-coordinate to center the image
|
||||||
@@ -238,7 +297,20 @@ public class PdfUtils {
|
|||||||
// Zip the images and return as byte array
|
// Zip the images and return as byte array
|
||||||
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
|
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
|
||||||
for (int i = 0; i < pageCount; ++i) {
|
for (int i = 0; i < pageCount; ++i) {
|
||||||
BufferedImage image = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
BufferedImage image;
|
||||||
|
try {
|
||||||
|
image = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
if (e.getMessage() != null
|
||||||
|
&& e.getMessage().contains("Maximum size of image exceeded")) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.pageTooBigForDpi",
|
||||||
|
"PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).",
|
||||||
|
i + 1,
|
||||||
|
DPI);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
try (ByteArrayOutputStream baosImage = new ByteArrayOutputStream()) {
|
try (ByteArrayOutputStream baosImage = new ByteArrayOutputStream()) {
|
||||||
ImageIO.write(image, imageType, baosImage);
|
ImageIO.write(image, imageType, baosImage);
|
||||||
|
|
||||||
@@ -276,7 +348,19 @@ public class PdfUtils {
|
|||||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
pdfRenderer.setSubsamplingAllowed(true);
|
pdfRenderer.setSubsamplingAllowed(true);
|
||||||
for (int page = 0; page < document.getNumberOfPages(); ++page) {
|
for (int page = 0; page < document.getNumberOfPages(); ++page) {
|
||||||
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
|
BufferedImage bim;
|
||||||
|
try {
|
||||||
|
bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
if (e.getMessage() != null
|
||||||
|
&& e.getMessage().contains("Maximum size of image exceeded")) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.pageTooBigFor300Dpi",
|
||||||
|
"PDF page {0} is too large to render at 300 DPI. The resulting image would exceed Java's maximum array size. Please use a lower DPI value for PDF-to-image conversion.",
|
||||||
|
page + 1);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
PDPage originalPage = document.getPage(page);
|
PDPage originalPage = document.getPage(page);
|
||||||
|
|
||||||
float width = originalPage.getMediaBox().getWidth();
|
float width = originalPage.getMediaBox().getWidth();
|
||||||
@@ -349,7 +433,7 @@ public class PdfUtils {
|
|||||||
}
|
}
|
||||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||||
doc.save(byteArrayOutputStream);
|
doc.save(byteArrayOutputStream);
|
||||||
log.info("PDF successfully saved to byte array");
|
log.debug("PDF successfully saved to byte array");
|
||||||
return byteArrayOutputStream.toByteArray();
|
return byteArrayOutputStream.toByteArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,8 +579,7 @@ public class PdfUtils {
|
|||||||
case "less":
|
case "less":
|
||||||
return actualPageCount < pageCount;
|
return actualPageCount < pageCount;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException(
|
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
|
||||||
"Invalid comparator. Only 'greater', 'equal', and 'less' are supported.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,16 @@ public class ProcessExecutor {
|
|||||||
.getProcessExecutor()
|
.getProcessExecutor()
|
||||||
.getSessionLimit()
|
.getSessionLimit()
|
||||||
.getCalibreSessionLimit();
|
.getCalibreSessionLimit();
|
||||||
|
case GHOSTSCRIPT ->
|
||||||
|
applicationProperties
|
||||||
|
.getProcessExecutor()
|
||||||
|
.getSessionLimit()
|
||||||
|
.getGhostscriptSessionLimit();
|
||||||
|
case OCR_MY_PDF ->
|
||||||
|
applicationProperties
|
||||||
|
.getProcessExecutor()
|
||||||
|
.getSessionLimit()
|
||||||
|
.getOcrMyPdfSessionLimit();
|
||||||
};
|
};
|
||||||
|
|
||||||
long timeoutMinutes =
|
long timeoutMinutes =
|
||||||
@@ -128,6 +138,16 @@ public class ProcessExecutor {
|
|||||||
.getProcessExecutor()
|
.getProcessExecutor()
|
||||||
.getTimeoutMinutes()
|
.getTimeoutMinutes()
|
||||||
.getCalibreTimeoutMinutes();
|
.getCalibreTimeoutMinutes();
|
||||||
|
case GHOSTSCRIPT ->
|
||||||
|
applicationProperties
|
||||||
|
.getProcessExecutor()
|
||||||
|
.getTimeoutMinutes()
|
||||||
|
.getGhostscriptTimeoutMinutes();
|
||||||
|
case OCR_MY_PDF ->
|
||||||
|
applicationProperties
|
||||||
|
.getProcessExecutor()
|
||||||
|
.getTimeoutMinutes()
|
||||||
|
.getOcrMyPdfTimeoutMinutes();
|
||||||
};
|
};
|
||||||
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
|
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
|
||||||
});
|
});
|
||||||
@@ -278,7 +298,9 @@ public class ProcessExecutor {
|
|||||||
INSTALL_APP,
|
INSTALL_APP,
|
||||||
CALIBRE,
|
CALIBRE,
|
||||||
TESSERACT,
|
TESSERACT,
|
||||||
QPDF
|
QPDF,
|
||||||
|
GHOSTSCRIPT,
|
||||||
|
OCR_MY_PDF
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ProcessExecutorResult {
|
public class ProcessExecutorResult {
|
||||||
|
|||||||
+6
@@ -108,6 +108,12 @@ public class CustomColorReplaceStrategy extends ReplaceAndInvertColorStrategy {
|
|||||||
} catch (IllegalArgumentException ie) {
|
} catch (IllegalArgumentException ie) {
|
||||||
log.info("text not supported by font ");
|
log.info("text not supported by font ");
|
||||||
font = checkSupportedFontForCharacter(unicodeText);
|
font = checkSupportedFontForCharacter(unicodeText);
|
||||||
|
} catch (UnsupportedOperationException ue) {
|
||||||
|
log.info(
|
||||||
|
"font does not support encoding operation: {} for text: '{}'",
|
||||||
|
font.getClass().getSimpleName(),
|
||||||
|
unicodeText);
|
||||||
|
font = checkSupportedFontForCharacter(unicodeText);
|
||||||
} finally {
|
} finally {
|
||||||
// if any other font is not supported, then replace default character *
|
// if any other font is not supported, then replace default character *
|
||||||
if (font == null) {
|
if (font == null) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import org.springframework.test.util.ReflectionTestUtils;
|
|||||||
|
|
||||||
import stirling.software.common.model.job.JobResult;
|
import stirling.software.common.model.job.JobResult;
|
||||||
import stirling.software.common.model.job.JobStats;
|
import stirling.software.common.model.job.JobStats;
|
||||||
|
import stirling.software.common.model.job.ResultFile;
|
||||||
|
|
||||||
class TaskManagerTest {
|
class TaskManagerTest {
|
||||||
|
|
||||||
@@ -73,13 +74,17 @@ class TaskManagerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSetFileResult() {
|
void testSetFileResult() throws Exception {
|
||||||
// Arrange
|
// Arrange
|
||||||
String jobId = UUID.randomUUID().toString();
|
String jobId = UUID.randomUUID().toString();
|
||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
String fileId = "file-id";
|
String fileId = "file-id";
|
||||||
String originalFileName = "test.pdf";
|
String originalFileName = "test.pdf";
|
||||||
String contentType = "application/pdf";
|
String contentType = "application/pdf";
|
||||||
|
long fileSize = 1024L;
|
||||||
|
|
||||||
|
// Mock the fileStorage.getFileSize() call
|
||||||
|
when(fileStorage.getFileSize(fileId)).thenReturn(fileSize);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
taskManager.setFileResult(jobId, fileId, originalFileName, contentType);
|
taskManager.setFileResult(jobId, fileId, originalFileName, contentType);
|
||||||
@@ -88,9 +93,17 @@ class TaskManagerTest {
|
|||||||
JobResult result = taskManager.getJobResult(jobId);
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
assertNotNull(result);
|
assertNotNull(result);
|
||||||
assertTrue(result.isComplete());
|
assertTrue(result.isComplete());
|
||||||
assertEquals(fileId, result.getFileId());
|
assertTrue(result.hasFiles());
|
||||||
assertEquals(originalFileName, result.getOriginalFileName());
|
assertFalse(result.hasMultipleFiles());
|
||||||
assertEquals(contentType, result.getContentType());
|
|
||||||
|
var resultFiles = result.getAllResultFiles();
|
||||||
|
assertEquals(1, resultFiles.size());
|
||||||
|
|
||||||
|
ResultFile resultFile = resultFiles.get(0);
|
||||||
|
assertEquals(fileId, resultFile.getFileId());
|
||||||
|
assertEquals(originalFileName, resultFile.getFileName());
|
||||||
|
assertEquals(contentType, resultFile.getContentType());
|
||||||
|
assertEquals(fileSize, resultFile.getFileSize());
|
||||||
assertNotNull(result.getCompletedAt());
|
assertNotNull(result.getCompletedAt());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,8 +176,11 @@ class TaskManagerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testGetJobStats() {
|
void testGetJobStats() throws Exception {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
// Mock fileStorage.getFileSize for file operations
|
||||||
|
when(fileStorage.getFileSize("file-id")).thenReturn(1024L);
|
||||||
|
|
||||||
// 1. Create active job
|
// 1. Create active job
|
||||||
String activeJobId = "active-job";
|
String activeJobId = "active-job";
|
||||||
taskManager.createTask(activeJobId);
|
taskManager.createTask(activeJobId);
|
||||||
@@ -216,9 +232,15 @@ class TaskManagerTest {
|
|||||||
LocalDateTime oldTime = LocalDateTime.now().minusHours(1);
|
LocalDateTime oldTime = LocalDateTime.now().minusHours(1);
|
||||||
ReflectionTestUtils.setField(oldJob, "completedAt", oldTime);
|
ReflectionTestUtils.setField(oldJob, "completedAt", oldTime);
|
||||||
ReflectionTestUtils.setField(oldJob, "complete", true);
|
ReflectionTestUtils.setField(oldJob, "complete", true);
|
||||||
ReflectionTestUtils.setField(oldJob, "fileId", "file-id");
|
|
||||||
ReflectionTestUtils.setField(oldJob, "originalFileName", "test.pdf");
|
// Create a ResultFile and set it using the new approach
|
||||||
ReflectionTestUtils.setField(oldJob, "contentType", "application/pdf");
|
ResultFile resultFile = ResultFile.builder()
|
||||||
|
.fileId("file-id")
|
||||||
|
.fileName("test.pdf")
|
||||||
|
.contentType("application/pdf")
|
||||||
|
.fileSize(1024L)
|
||||||
|
.build();
|
||||||
|
ReflectionTestUtils.setField(oldJob, "resultFiles", java.util.List.of(resultFile));
|
||||||
|
|
||||||
when(fileStorage.deleteFile("file-id")).thenReturn(true);
|
when(fileStorage.deleteFile("file-id")).thenReturn(true);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,588 @@
|
|||||||
|
# Stirling-PDF Developer Guide
|
||||||
|
|
||||||
|
## 1. Introduction
|
||||||
|
|
||||||
|
Stirling-PDF is a robust, locally hosted, web-based PDF manipulation tool. This guide focuses on Docker-based development and testing, which is the recommended approach for working with the full version of Stirling-PDF.
|
||||||
|
|
||||||
|
## 2. Project Overview
|
||||||
|
|
||||||
|
Stirling-PDF is built using:
|
||||||
|
|
||||||
|
- Spring Boot + Thymeleaf
|
||||||
|
- PDFBox
|
||||||
|
- LibreOffice
|
||||||
|
- qpdf
|
||||||
|
- HTML, CSS, JavaScript
|
||||||
|
- Docker
|
||||||
|
- PDF.js
|
||||||
|
- PDF-LIB.js
|
||||||
|
- Lombok
|
||||||
|
|
||||||
|
## 3. Development Environment Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker
|
||||||
|
- Git
|
||||||
|
- Java JDK 17 or later
|
||||||
|
- Gradle 7.0 or later (Included within the repo)
|
||||||
|
|
||||||
|
### Setup Steps
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/Stirling-Tools/Stirling-PDF.git
|
||||||
|
cd Stirling-PDF
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install Docker and JDK17 if not already installed.
|
||||||
|
|
||||||
|
3. Install a recommended Java IDE such as Eclipse, IntelliJ, or VSCode
|
||||||
|
1. Only VSCode
|
||||||
|
1. Open VS Code.
|
||||||
|
2. When prompted, install the recommended extensions.
|
||||||
|
3. Alternatively, open the command palette (`Ctrl + Shift + P` or `Cmd + Shift + P` on macOS) and run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
Extensions: Show Recommended Extensions
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Install the required extensions from the list.
|
||||||
|
|
||||||
|
4. Lombok Setup
|
||||||
|
Stirling-PDF uses Lombok to reduce boilerplate code. Some IDEs, like Eclipse, don't support Lombok out of the box. To set up Lombok in your development environment:
|
||||||
|
Visit the [Lombok website](https://projectlombok.org/setup/) for installation instructions specific to your IDE.
|
||||||
|
|
||||||
|
5. Add environment variable
|
||||||
|
For local testing, you should generally be testing the full 'Security' version of Stirling PDF. To do this, you must add the environment flag DISABLE_ADDITIONAL_FEATURES=false to your system and/or IDE build/run step.
|
||||||
|
|
||||||
|
## 4. Project Structure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Stirling-PDF/
|
||||||
|
├── .github/ # GitHub-specific files (workflows, issue templates)
|
||||||
|
├── configs/ # Configuration files used by stirling at runtime (generated at runtime)
|
||||||
|
├── cucumber/ # Cucumber test files
|
||||||
|
│ ├── features/
|
||||||
|
├── customFiles/ # Custom static files and templates (generated at runtime used to replace existing files)
|
||||||
|
├── docs/ # Documentation files
|
||||||
|
├── exampleYmlFiles/ # Example YAML configuration files
|
||||||
|
├── images/ # Image assets
|
||||||
|
├── pipeline/ # Pipeline-related files (generated at runtime)
|
||||||
|
├── scripts/ # Utility scripts
|
||||||
|
├── src/ # Source code
|
||||||
|
│ ├── main/
|
||||||
|
│ │ ├── java/
|
||||||
|
│ │ │ └── stirling/
|
||||||
|
│ │ │ └── software/
|
||||||
|
│ │ │ └── SPDF/
|
||||||
|
│ │ │ ├── config/
|
||||||
|
│ │ │ ├── controller/
|
||||||
|
│ │ │ ├── model/
|
||||||
|
│ │ │ ├── repository/
|
||||||
|
│ │ │ ├── service/
|
||||||
|
│ │ │ └── utils/
|
||||||
|
│ │ └── resources/
|
||||||
|
│ │ ├── static/
|
||||||
|
│ │ │ ├── css/
|
||||||
|
│ │ │ ├── js/
|
||||||
|
│ │ │ └── pdfjs/
|
||||||
|
│ │ └── templates/
|
||||||
|
│ └── test/
|
||||||
|
│ └── java/
|
||||||
|
│ └── stirling/
|
||||||
|
│ └── software/
|
||||||
|
│ └── SPDF/
|
||||||
|
├── build.gradle # Gradle build configuration
|
||||||
|
├── Dockerfile # Main Dockerfile
|
||||||
|
├── Dockerfile.ultra-lite # Dockerfile for ultra-lite version
|
||||||
|
├── Dockerfile.fat # Dockerfile for fat version
|
||||||
|
├── docker-compose.yml # Docker Compose configuration
|
||||||
|
└── test.sh # Test script to deploy all docker versions and run cuke tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Docker-based Development
|
||||||
|
|
||||||
|
Stirling-PDF offers several Docker versions:
|
||||||
|
|
||||||
|
- Full: All features included
|
||||||
|
- Ultra-Lite: Basic PDF operations only
|
||||||
|
- Fat: Includes additional libraries and fonts predownloaded
|
||||||
|
|
||||||
|
### Example Docker Compose Files
|
||||||
|
|
||||||
|
Stirling-PDF provides several example Docker Compose files in the `exampleYmlFiles` directory, such as:
|
||||||
|
|
||||||
|
- `docker-compose-latest.yml`: Latest version without login and security features
|
||||||
|
- `docker-compose-latest-security.yml`: Latest version with login and security features enabled
|
||||||
|
- `docker-compose-latest-fat-security.yml`: Fat version with login and security features enabled
|
||||||
|
|
||||||
|
These files provide pre-configured setups for different scenarios. For example, here's a snippet from `docker-compose-latest-security.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
stirling-pdf:
|
||||||
|
container_name: Stirling-PDF-Security
|
||||||
|
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 4G
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 16
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
- ./stirling/latest/data:/usr/share/tessdata:rw
|
||||||
|
- ./stirling/latest/config:/configs:rw
|
||||||
|
- ./stirling/latest/logs:/logs:rw
|
||||||
|
environment:
|
||||||
|
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||||
|
SECURITY_ENABLELOGIN: "true"
|
||||||
|
PUID: 1002
|
||||||
|
PGID: 1002
|
||||||
|
UMASK: "022"
|
||||||
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
|
UI_APPNAME: Stirling-PDF
|
||||||
|
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest with Security
|
||||||
|
UI_APPNAMENAVBAR: Stirling-PDF Latest
|
||||||
|
SYSTEM_MAXFILESIZE: "100"
|
||||||
|
METRICS_ENABLED: "true"
|
||||||
|
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||||
|
SHOW_SURVEY: "true"
|
||||||
|
restart: on-failure:5
|
||||||
|
```
|
||||||
|
|
||||||
|
To use these example files, copy the desired file to your project root and rename it to `docker-compose.yml`, or specify the file explicitly when running Docker Compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f exampleYmlFiles/docker-compose-latest-security.yml up
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building Docker Images
|
||||||
|
|
||||||
|
Stirling-PDF uses different Docker images for various configurations. The build process is controlled by environment variables and uses specific Dockerfile variants. Here's how to build the Docker images:
|
||||||
|
|
||||||
|
1. Set the security environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export DISABLE_ADDITIONAL_FEATURES=true # or false for to enable login and security features for builds
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Build the project with Gradle:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew clean build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Build the Docker images:
|
||||||
|
|
||||||
|
For the latest version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .
|
||||||
|
```
|
||||||
|
|
||||||
|
For the ultra-lite version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite .
|
||||||
|
```
|
||||||
|
|
||||||
|
For the fat version (with login and security features enabled):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export DISABLE_ADDITIONAL_FEATURES=false
|
||||||
|
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat .
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase
|
||||||
|
|
||||||
|
## 6. Testing
|
||||||
|
|
||||||
|
### Comprehensive Testing Script
|
||||||
|
|
||||||
|
Stirling-PDF provides a `test.sh` script in the root directory. This script builds all versions of Stirling-PDF, checks that each version works, and runs Cucumber tests. It's recommended to run this script before submitting a final pull request.
|
||||||
|
|
||||||
|
To run the test script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./test.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This script performs the following actions:
|
||||||
|
|
||||||
|
1. Builds all Docker images (full, ultra-lite, fat).
|
||||||
|
2. Runs each version to ensure it starts correctly.
|
||||||
|
3. Executes Cucumber tests against the main version and ensures feature compatibility. In the event these tests fail, your PR will not be merged.
|
||||||
|
|
||||||
|
Note: The `test.sh` script will run automatically when you raise a PR. However, it's recommended to run it locally first to save resources and catch any issues early.
|
||||||
|
|
||||||
|
### Full Testing with Docker
|
||||||
|
|
||||||
|
1. Build and run the Docker container per the above instructions:
|
||||||
|
|
||||||
|
2. Access the application at `http://localhost:8080` and manually test all features developed.
|
||||||
|
|
||||||
|
### Local Testing (Java and UI Components)
|
||||||
|
|
||||||
|
For quick iterations and development of Java backend, JavaScript, and UI components, you can run and test Stirling-PDF locally without Docker. This approach allows you to work on and verify changes to:
|
||||||
|
|
||||||
|
- Java backend logic
|
||||||
|
- RESTful API endpoints
|
||||||
|
- JavaScript functionality
|
||||||
|
- User interface components and styling
|
||||||
|
- Thymeleaf templates
|
||||||
|
|
||||||
|
To run Stirling-PDF locally:
|
||||||
|
|
||||||
|
1. Compile and run the project using built-in IDE methods or by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew bootRun
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Access the application at `http://localhost:8080` in your web browser.
|
||||||
|
|
||||||
|
3. Manually test the features you're working on through the UI.
|
||||||
|
|
||||||
|
4. For API changes, use tools like Postman or curl to test endpoints directly.
|
||||||
|
|
||||||
|
Important notes:
|
||||||
|
|
||||||
|
- Local testing doesn't include features that depend on external tools like qpdf, LibreOffice, or Python scripts.
|
||||||
|
- There are currently no automated unit tests. All testing is done manually through the UI or API calls. (You are welcome to add JUnits!)
|
||||||
|
- Always verify your changes in the full Docker environment before submitting pull requests, as some integrations and features will only work in the complete setup.
|
||||||
|
|
||||||
|
## 7. Contributing
|
||||||
|
|
||||||
|
1. Fork the repository on GitHub.
|
||||||
|
2. Create a new branch for your feature or bug fix.
|
||||||
|
3. Make your changes and commit them with clear, descriptive messages and ensure any documentation is updated related to your changes.
|
||||||
|
4. Test your changes thoroughly in the Docker environment.
|
||||||
|
5. Run the `test.sh` script to ensure all versions build correctly and pass the Cucumber tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./test.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Push your changes to your fork.
|
||||||
|
7. Submit a pull request to the main repository.
|
||||||
|
8. See additional [contributing guidelines](../CONTRIBUTING.md).
|
||||||
|
|
||||||
|
When you raise a PR:
|
||||||
|
|
||||||
|
- The `test.sh` script will run automatically against your PR.
|
||||||
|
- The PR checks will verify versioning and dependency updates.
|
||||||
|
- Documentation will be automatically updated for dependency changes.
|
||||||
|
- Security issues will be checked using Snyk and PixeeBot.
|
||||||
|
|
||||||
|
Address any issues that arise from these checks before finalizing your pull request.
|
||||||
|
|
||||||
|
## 8. API Documentation
|
||||||
|
|
||||||
|
API documentation is available at `/swagger-ui/index.html` when running the application. You can also view the latest API documentation [here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/).
|
||||||
|
|
||||||
|
## 9. Customization
|
||||||
|
|
||||||
|
Stirling-PDF can be customized through environment variables or a `settings.yml` file. Key customization options include:
|
||||||
|
|
||||||
|
- Application name and branding
|
||||||
|
- Security settings
|
||||||
|
- UI customization
|
||||||
|
- Endpoint management
|
||||||
|
|
||||||
|
When using Docker, pass environment variables using the `-e` flag or in your `docker-compose.yml` file.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 8080:8080 -e APP_NAME="My PDF Tool" stirling-pdf:full
|
||||||
|
```
|
||||||
|
|
||||||
|
Refer to the main README for a full list of customization options.
|
||||||
|
|
||||||
|
## 10. Language Translations
|
||||||
|
|
||||||
|
For managing language translations that affect multiple files, Stirling-PDF provides a helper script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/scripts/replace_translation_line.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This script helps you make consistent replacements across language files.
|
||||||
|
|
||||||
|
When contributing translations:
|
||||||
|
|
||||||
|
1. Use the helper script for multi-file changes.
|
||||||
|
2. Ensure all language files are updated consistently.
|
||||||
|
3. The PR checks will verify consistency in language file updates.
|
||||||
|
|
||||||
|
Remember to test your changes thoroughly to ensure they don't break any existing functionality.
|
||||||
|
|
||||||
|
## Code examples
|
||||||
|
|
||||||
|
### Overview of Thymeleaf
|
||||||
|
|
||||||
|
Thymeleaf is a server-side Java HTML template engine. It is used in Stirling-PDF to render dynamic web pages. Thymeleaf integrates heavily with Spring Boot.
|
||||||
|
|
||||||
|
### Thymeleaf overview
|
||||||
|
|
||||||
|
In Stirling-PDF, Thymeleaf is used to create HTML templates that are rendered on the server side. These templates are located in the `stirling-pdf/src/main/resources/templates` directory. Thymeleaf templates use a combination of HTML and special Thymeleaf attributes to dynamically generate content.
|
||||||
|
|
||||||
|
Some examples of this are:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||||
|
```
|
||||||
|
or
|
||||||
|
```html
|
||||||
|
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||||
|
```
|
||||||
|
|
||||||
|
Where it uses the `th:block`, `th:` indicating it's a special Thymeleaf element to be used server-side in generating the HTML, and block being the actual element type.
|
||||||
|
In this case, we are inserting the `navbar` entry within the `fragments/navbar.html` fragment into the `th:block` element.
|
||||||
|
|
||||||
|
They can be more complex, such as:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<th:block th:insert="~{fragments/common :: head(title=#{pageExtracter.title}, header=#{pageExtracter.header})}"></th:block>
|
||||||
|
```
|
||||||
|
|
||||||
|
Which is the same as above but passes the parameters title and header into the fragment `common.html` to be used in its HTML generation.
|
||||||
|
|
||||||
|
Thymeleaf can also be used to loop through objects or pass things from the Java side into the HTML side.
|
||||||
|
|
||||||
|
```java
|
||||||
|
@GetMapping
|
||||||
|
public String newFeaturePage(Model model) {
|
||||||
|
model.addAttribute("exampleData", exampleData);
|
||||||
|
return "new-feature";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above example, if exampleData is a list of plain java objects of class Person and within it, you had id, name, age, etc. You can reference it like so
|
||||||
|
|
||||||
|
```html
|
||||||
|
<tbody>
|
||||||
|
<!-- Use th:each to iterate over the list -->
|
||||||
|
<tr th:each="person : ${exampleData}">
|
||||||
|
<td th:text="${person.id}"></td>
|
||||||
|
<td th:text="${person.name}"></td>
|
||||||
|
<td th:text="${person.age}"></td>
|
||||||
|
<td th:text="${person.email}"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
```
|
||||||
|
|
||||||
|
This would generate n entries of tr for each person in exampleData
|
||||||
|
|
||||||
|
### Adding a New Feature to the Backend (API)
|
||||||
|
|
||||||
|
1. **Create a New Controller:**
|
||||||
|
- Create a new Java class in the `stirling-pdf/src/main/java/stirling/software/SPDF/controller/api` directory.
|
||||||
|
- Annotate the class with `@RestController` and `@RequestMapping` to define the API endpoint.
|
||||||
|
- Ensure to add API documentation annotations like `@Tag(name = "General", description = "General APIs")` and `@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/new-feature")
|
||||||
|
@Tag(name = "General", description = "General APIs")
|
||||||
|
public class NewFeatureController {
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
|
||||||
|
public String newFeature() {
|
||||||
|
return "NewFeatureResponse"; // This refers to the NewFeatureResponse.html template presenting the user with the generated html from that file when they navigate to /api/v1/new-feature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Define the Service Layer:** (Not required but often useful)
|
||||||
|
- Create a new service class in the `stirling-pdf/src/main/java/stirling/software/SPDF/service` directory.
|
||||||
|
- Implement the business logic for the new feature.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package stirling.software.SPDF.service;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class NewFeatureService {
|
||||||
|
|
||||||
|
public String getNewFeatureData() {
|
||||||
|
// Implement business logic here
|
||||||
|
return "New Feature Data";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2b. **Integrate the Service with the Controller:**
|
||||||
|
|
||||||
|
- Autowire the service class in the controller and use it to handle the API request.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import stirling.software.SPDF.service.NewFeatureService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/new-feature")
|
||||||
|
@Tag(name = "General", description = "General APIs")
|
||||||
|
public class NewFeatureController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private NewFeatureService newFeatureService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
|
||||||
|
public String newFeature() {
|
||||||
|
return newFeatureService.getNewFeatureData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding a New Feature to the Frontend (UI)
|
||||||
|
|
||||||
|
1. **Create a New Thymeleaf Template:**
|
||||||
|
- Create a new HTML file in the `stirling-pdf/src/main/resources/templates` directory.
|
||||||
|
- Use Thymeleaf attributes to dynamically generate content.
|
||||||
|
- Use `extract-page.html` as a base example for the HTML template, which is useful to ensure importing of the general layout, navbar, and footer.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<th:block th:insert="~{fragments/common :: head(title=#{newFeature.title}, header=#{newFeature.header})}"></th:block>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="page-container">
|
||||||
|
<div id="content-wrap">
|
||||||
|
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||||
|
<br><br>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6 bg-card">
|
||||||
|
<div class="tool-header">
|
||||||
|
<span class="material-symbols-rounded tool-header-icon organize">upload</span>
|
||||||
|
<span class="tool-header-text" th:text="#{newFeature.header}"></span>
|
||||||
|
</div>
|
||||||
|
<form th:action="@{'/api/v1/new-feature'}" method="post" enctype="multipart/form-data">
|
||||||
|
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||||
|
<input type="hidden" id="customMode" name="customMode" value="">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="featureInput" th:text="#{newFeature.prompt}"></label>
|
||||||
|
<input type="text" class="form-control" id="featureInput" name="featureInput" th:placeholder="#{newFeature.placeholder}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{newFeature.submit}"></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create a New Controller for the UI:**
|
||||||
|
- Create a new Java class in the `stirling-pdf/src/main/java/stirling/software/SPDF/controller/ui` directory.
|
||||||
|
- Annotate the class with `@Controller` and `@RequestMapping` to define the UI endpoint.
|
||||||
|
|
||||||
|
```java
|
||||||
|
package stirling.software.SPDF.controller.ui;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import stirling.software.SPDF.service.NewFeatureService;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/new-feature")
|
||||||
|
public class NewFeatureUIController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private NewFeatureService newFeatureService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String newFeaturePage(Model model) {
|
||||||
|
model.addAttribute("newFeatureData", newFeatureService.getNewFeatureData());
|
||||||
|
return "new-feature";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Update the Navigation Bar:**
|
||||||
|
- Add a link to the new feature page in the navigation bar.
|
||||||
|
- Update the `stirling-pdf/src/main/resources/templates/fragments/navbar.html` file.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" th:href="@{'/new-feature'}">New Feature</a>
|
||||||
|
</li>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding New Translations to Existing Language Files in Stirling-PDF
|
||||||
|
|
||||||
|
When adding a new feature or modifying existing ones in Stirling-PDF, you'll need to add new translation entries to the existing language files. Here's a step-by-step guide:
|
||||||
|
|
||||||
|
### 1. Locate Existing Language Files
|
||||||
|
|
||||||
|
Find the existing `messages.properties` files in the `stirling-pdf/src/main/resources` directory. You'll see files like:
|
||||||
|
|
||||||
|
- `messages.properties` (default, usually English)
|
||||||
|
- `messages_en_GB.properties`
|
||||||
|
- `messages_fr_FR.properties`
|
||||||
|
- `messages_de_DE.properties`
|
||||||
|
- etc.
|
||||||
|
|
||||||
|
### 2. Add New Translation Entries
|
||||||
|
|
||||||
|
Open each of these files and add your new translation entries. For example, if you're adding a new feature called "PDF Splitter",
|
||||||
|
Use descriptive, hierarchical keys (e.g., `feature.element.description`)
|
||||||
|
you might add:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
pdfSplitter.title=PDF Splitter
|
||||||
|
pdfSplitter.description=Split your PDF into multiple documents
|
||||||
|
pdfSplitter.button.split=Split PDF
|
||||||
|
pdfSplitter.input.pages=Enter page numbers to split
|
||||||
|
```
|
||||||
|
|
||||||
|
Add these entries to the default GB language file and any others you wish, translating the values as appropriate for each language.
|
||||||
|
|
||||||
|
### 3. Use Translations in Thymeleaf Templates
|
||||||
|
|
||||||
|
In your Thymeleaf templates, use the `#{key}` syntax to reference the new translations:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<h1 th:text="#{pdfSplitter.title}">PDF Splitter</h1>
|
||||||
|
<p th:text="#{pdfSplitter.description}">Split your PDF into multiple documents</p>
|
||||||
|
<input type="text" th:placeholder="#{pdfSplitter.input.pages}">
|
||||||
|
<button th:text="#{pdfSplitter.button.split}">Split PDF</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember, never hard-code text in your templates or Java code. Always use translation keys to ensure proper localization.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Exception Handling Guide
|
||||||
|
|
||||||
|
This guide outlines the common error handling patterns used within Stirling-PDF and provides tips for internationalising error messages. The examples cover the main languages found in the project: Java, JavaScript, HTML/CSS, and a small amount of Python.
|
||||||
|
|
||||||
|
## General Principles
|
||||||
|
|
||||||
|
- **Fail fast and log clearly.** Exceptions should provide enough information for debugging without exposing sensitive data.
|
||||||
|
- **Use consistent user messages.** Text shown to users must be pulled from the localisation files so that translations are centrally managed.
|
||||||
|
- **Avoid silent failures.** Always log unexpected errors and provide the user with a helpful message.
|
||||||
|
|
||||||
|
## Java
|
||||||
|
|
||||||
|
Java forms the core of Stirling-PDF. When adding new features or handling errors:
|
||||||
|
|
||||||
|
1. **Create custom exceptions** to represent specific failure cases. This keeps the code self-documenting and easier to handle at higher levels.
|
||||||
|
2. **Use `try-with-resources`** when working with streams or other closable resources to ensure clean-up even on failure.
|
||||||
|
3. **Return meaningful HTTP status codes** in controllers by throwing `ResponseStatusException` or using `@ExceptionHandler` methods.
|
||||||
|
4. **Log with context** using the project’s logging framework. Include identifiers or IDs that help trace the issue.
|
||||||
|
5. **Internationalise messages** by placing user-facing text in `messages_en_GB.properties` and referencing them with message keys.
|
||||||
|
|
||||||
|
## JavaScript
|
||||||
|
|
||||||
|
On the client side, JavaScript handles form validation and user interactions.
|
||||||
|
|
||||||
|
- Use `try`/`catch` around asynchronous operations (e.g., `fetch`) and display a translated error notice if the call fails.
|
||||||
|
- Validate input before sending it to the server and provide inline feedback with messages from the translation files.
|
||||||
|
- Log unexpected errors to the browser console for easier debugging, but avoid revealing sensitive information.
|
||||||
|
|
||||||
|
## HTML & CSS
|
||||||
|
|
||||||
|
HTML templates should reserve a space for displaying error messages. Example pattern:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="error" role="alert" th:text="${errorMessage}"></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Use CSS classes (e.g., `.error`) to style the message so it is clearly visible and accessible. Keep the markup simple to ensure screen readers can announce the error correctly.
|
||||||
|
|
||||||
|
## Python
|
||||||
|
|
||||||
|
Python scripts in this project are mainly for utility tasks. Follow these guidelines:
|
||||||
|
|
||||||
|
- Wrap file operations or external calls in `try`/`except` blocks.
|
||||||
|
- Print or log errors in a consistent format. If the script outputs messages to end users, ensure they are translatable.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
perform_task()
|
||||||
|
except Exception as err:
|
||||||
|
logger.error("Task failed: %s", err)
|
||||||
|
print(gettext("task.error"))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Internationalisation (i18n)
|
||||||
|
|
||||||
|
All user-visible error strings should be defined in the main translation file (`messages_en_GB.properties`). Other language files will use the same keys. Refer to messages in code rather than hard-coding text.
|
||||||
|
|
||||||
|
When creating new messages:
|
||||||
|
|
||||||
|
1. Add the English phrase to `messages_en_GB.properties`.
|
||||||
|
2. Reference the message key in your Java, JavaScript, or Python code.
|
||||||
|
3. Update other localisation files as needed.
|
||||||
|
|
||||||
|
Following these patterns helps keep Stirling-PDF stable, easier to debug, and friendly to users in all supported languages.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Developer Guide Directory
|
||||||
|
|
||||||
|
This directory contains all development-related documentation for Stirling PDF.
|
||||||
|
|
||||||
|
## 📚 Documentation Index
|
||||||
|
|
||||||
|
### Core Development
|
||||||
|
- **[DeveloperGuide.md](./DeveloperGuide.md)** - Main developer setup and architecture guide
|
||||||
|
- **[EXCEPTION_HANDLING_GUIDE.md](./EXCEPTION_HANDLING_GUIDE.md)** - Exception handling patterns and i18n best practices
|
||||||
|
- **[HowToAddNewLanguage.md](./HowToAddNewLanguage.md)** - Internationalization and translation guide
|
||||||
|
|
||||||
|
### Features & Documentation
|
||||||
|
- **[AGENTS.md](./AGENTS.md)** - Agent-based functionality documentation
|
||||||
|
- **[USERS.md](./USERS.md)** - User-focused documentation and guides
|
||||||
|
|
||||||
|
## 🔗 Related Files in Root
|
||||||
|
- **[README.md](../README.md)** - Project overview and quick start
|
||||||
|
- **[CONTRIBUTING.md](../CONTRIBUTING.md)** - Contribution guidelines
|
||||||
|
- **[SECURITY.md](../SECURITY.md)** - Security policies and reporting
|
||||||
|
- **[DATABASE.md](../DATABASE.md)** - Database setup and configuration (usage guide)
|
||||||
|
- **[HowToUseOCR.md](../HowToUseOCR.md)** - OCR setup and configuration (usage guide)
|
||||||
|
|
||||||
|
## 📝 Contributing to Documentation
|
||||||
|
|
||||||
|
When adding new development documentation:
|
||||||
|
1. Place technical guides in this `devGuide/` directory
|
||||||
|
2. Update this index file with a brief description
|
||||||
|
3. Keep user-facing docs (README, CONTRIBUTING, SECURITY) in the root
|
||||||
|
4. Follow existing naming conventions (PascalCase for guides)
|
||||||
@@ -17,7 +17,6 @@ services:
|
|||||||
- ./stirling/latest/config:/configs:rw
|
- ./stirling/latest/config:/configs:rw
|
||||||
- ./stirling/latest/logs:/logs:rw
|
- ./stirling/latest/logs:/logs:rw
|
||||||
environment:
|
environment:
|
||||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
|
||||||
SECURITY_ENABLELOGIN: "false"
|
SECURITY_ENABLELOGIN: "false"
|
||||||
SYSTEM_DEFAULTLOCALE: en-US
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
UI_APPNAME: Stirling-PDF-Ultra-lite
|
UI_APPNAME: Stirling-PDF-Ultra-lite
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ services:
|
|||||||
- ./stirling/latest/config:/configs:rw
|
- ./stirling/latest/config:/configs:rw
|
||||||
- ./stirling/latest/logs:/logs:rw
|
- ./stirling/latest/logs:/logs:rw
|
||||||
environment:
|
environment:
|
||||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
|
||||||
SECURITY_ENABLELOGIN: "false"
|
SECURITY_ENABLELOGIN: "false"
|
||||||
LANGS: "en_GB,en_US,ar_AR,de_DE,fr_FR,es_ES,zh_CN,zh_TW,ca_CA,it_IT,sv_SE,pl_PL,ro_RO,ko_KR,pt_BR,ru_RU,el_GR,hi_IN,hu_HU,tr_TR,id_ID"
|
LANGS: "en_GB,en_US,ar_AR,de_DE,fr_FR,es_ES,zh_CN,zh_TW,ca_CA,it_IT,sv_SE,pl_PL,ro_RO,ko_KR,pt_BR,ru_RU,el_GR,hi_IN,hu_HU,tr_TR,id_ID"
|
||||||
SYSTEM_DEFAULTLOCALE: en-US
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ dependencies {
|
|||||||
api 'org.springframework.boot:spring-boot-starter-data-jpa'
|
api 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||||
api 'org.springframework.boot:spring-boot-starter-oauth2-client'
|
api 'org.springframework.boot:spring-boot-starter-oauth2-client'
|
||||||
api 'org.springframework.boot:spring-boot-starter-mail'
|
api 'org.springframework.boot:spring-boot-starter-mail'
|
||||||
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.33'
|
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.34'
|
||||||
implementation 'com.bucket4j:bucket4j_jdk17-core:8.14.0'
|
implementation 'com.bucket4j:bucket4j_jdk17-core:8.14.0'
|
||||||
|
|
||||||
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
|
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
|
||||||
|
|||||||
@@ -168,7 +168,7 @@
|
|||||||
<div class="data-form-group">
|
<div class="data-form-group">
|
||||||
<label for="userId" class="data-form-label" th:text="#{team.selectUser}">Select User</label>
|
<label for="userId" class="data-form-label" th:text="#{team.selectUser}">Select User</label>
|
||||||
<select name="userId" id="userId" class="data-form-control" required onchange="checkUserTeam(this.value)">
|
<select name="userId" id="userId" class="data-form-control" required onchange="checkUserTeam(this.value)">
|
||||||
<option value="" disabled selected th:text="#{selectFillter}">-- Select User --</option>
|
<option value="" disabled selected th:text="#{selectFilter}">-- Select User --</option>
|
||||||
<option th:each="user : ${availableUsers}" th:value="${user.id}" th:text="${user.username}"
|
<option th:each="user : ${availableUsers}" th:value="${user.id}" th:text="${user.username}"
|
||||||
th:data-team="${user.team != null ? user.team.name : ''}"
|
th:data-team="${user.team != null ? user.team.name : ''}"
|
||||||
th:data-team-id="${user.team != null ? user.team.id : ''}">
|
th:data-team-id="${user.team != null ? user.team.id : ''}">
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
echo "Running Stirling PDF with DISABLE_ADDITIONAL_FEATURES=${DISABLE_ADDITIONAL_FEATURES} and VERSION_TAG=${VERSION_TAG}"
|
echo "Running Stirling PDF with DISABLE_ADDITIONAL_FEATURES=${DISABLE_ADDITIONAL_FEATURES} and VERSION_TAG=${VERSION_TAG}"
|
||||||
# Check for DISABLE_ADDITIONAL_FEATURES and download the appropriate JAR if required
|
# Check for DISABLE_ADDITIONAL_FEATURES and download the appropriate JAR if required
|
||||||
if [ "$DISABLE_ADDITIONAL_FEATURES" = "false" ] && [ "$VERSION_TAG" != "alpha" ]; then
|
if [ "$VERSION_TAG" != "alpha" ] && [ "$VERSION_TAG" != "ALPHA" ]; then
|
||||||
if [ ! -f app-security.jar ]; then
|
if [ "$DISABLE_ADDITIONAL_FEATURES" = "false" ] || [ "$DISABLE_ADDITIONAL_FEATURES" = "FALSE" ] || [ "$DOCKER_ENABLE_SECURITY" = "true" ] || [ "$DOCKER_ENABLE_SECURITY" = "TRUE" ]; then
|
||||||
echo "Trying to download from: https://files.stirlingpdf.com/v$VERSION_TAG/Stirling-PDF-with-login.jar"
|
if [ ! -f app-security.jar ]; then
|
||||||
curl -L -o app-security.jar https://files.stirlingpdf.com/v$VERSION_TAG/Stirling-PDF-with-login.jar
|
echo "Trying to download from: https://files.stirlingpdf.com/v$VERSION_TAG/Stirling-PDF-with-login.jar"
|
||||||
|
curl -L -o app-security.jar https://files.stirlingpdf.com/v$VERSION_TAG/Stirling-PDF-with-login.jar
|
||||||
|
|
||||||
# If the first download attempt failed, try without the 'v' prefix
|
# If the first download attempt failed, try without the 'v' prefix
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "Trying to download from: https://files.stirlingpdf.com/$VERSION_TAG/Stirling-PDF-with-login.jar"
|
echo "Trying to download from: https://files.stirlingpdf.com/$VERSION_TAG/Stirling-PDF-with-login.jar"
|
||||||
curl -L -o app-security.jar https://files.stirlingpdf.com/$VERSION_TAG/Stirling-PDF-with-login.jar
|
curl -L -o app-security.jar https://files.stirlingpdf.com/$VERSION_TAG/Stirling-PDF-with-login.jar
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then # checks if curl was successful
|
if [ $? -eq 0 ]; then # checks if curl was successful
|
||||||
rm -f app.jar
|
rm -f app.jar
|
||||||
ln -s app-security.jar app.jar
|
ln -s app-security.jar app.jar
|
||||||
chown stirlingpdfuser:stirlingpdfgroup app.jar || true
|
chown stirlingpdfuser:stirlingpdfgroup app.jar || true
|
||||||
chmod 755 app.jar || true
|
chmod 755 app.jar || true
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
@echo off
|
||||||
|
REM --------------------------------------------------
|
||||||
|
REM Batch script to (re-)generate all requirements
|
||||||
|
REM with check for pip-compile and user confirmation
|
||||||
|
REM --------------------------------------------------
|
||||||
|
|
||||||
|
REM Check if pip-compile is available
|
||||||
|
pip-compile --version >nul 2>&1
|
||||||
|
if %ERRORLEVEL% neq 0 (
|
||||||
|
echo ERROR: pip-compile was not found.
|
||||||
|
echo Please install pip-tools:
|
||||||
|
echo pip install pip-tools
|
||||||
|
echo and ensure that pip-compile is in your PATH.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo pip-compile detected.
|
||||||
|
|
||||||
|
REM Prompt user for confirmation (default = Yes on ENTER)
|
||||||
|
set /p confirm="Do you want to generate all requirements? [Y/n] "
|
||||||
|
if /I "%confirm%"=="" set confirm=Y
|
||||||
|
|
||||||
|
if /I not "%confirm%"=="Y" (
|
||||||
|
echo Generation cancelled by user.
|
||||||
|
pause
|
||||||
|
exit /b 0
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Starting generation...
|
||||||
|
|
||||||
|
echo Generating .github\scripts\requirements_pre_commit.txt
|
||||||
|
pip-compile --generate-hashes --upgrade --strip-extras ^
|
||||||
|
--output-file=".github\scripts\requirements_pre_commit.txt" ^
|
||||||
|
".github\scripts\requirements_pre_commit.in"
|
||||||
|
|
||||||
|
echo Generating .github\scripts\requirements_sync_readme.txt
|
||||||
|
pip-compile --generate-hashes --upgrade --strip-extras ^
|
||||||
|
--output-file=".github\scripts\requirements_sync_readme.txt" ^
|
||||||
|
".github\scripts\requirements_sync_readme.in"
|
||||||
|
|
||||||
|
echo Generating testing\cucumber\requirements.txt
|
||||||
|
pip-compile --generate-hashes --upgrade --strip-extras ^
|
||||||
|
--output-file="testing\cucumber\requirements.txt" ^
|
||||||
|
"testing\cucumber\requirements.in"
|
||||||
|
|
||||||
|
echo All done!
|
||||||
|
pause
|
||||||
@@ -195,11 +195,19 @@ ignore = [
|
|||||||
'PDFToBook.selectText.1',
|
'PDFToBook.selectText.1',
|
||||||
'PDFToText.tags',
|
'PDFToText.tags',
|
||||||
'addPageNumbers.selectText.3',
|
'addPageNumbers.selectText.3',
|
||||||
|
'adminUserSettings.team',
|
||||||
'alphabet',
|
'alphabet',
|
||||||
|
'audit.dashboard.modal.id',
|
||||||
|
'audit.dashboard.status',
|
||||||
|
'audit.dashboard.tab.dashboard',
|
||||||
|
'audit.dashboard.tab.export',
|
||||||
|
'audit.dashboard.table.details',
|
||||||
|
'audit.dashboard.table.id',
|
||||||
'certSign.name',
|
'certSign.name',
|
||||||
'cookieBanner.popUp.acceptAllBtn',
|
'cookieBanner.popUp.acceptAllBtn',
|
||||||
'endpointStatistics.top10',
|
'endpointStatistics.top10',
|
||||||
'endpointStatistics.top20',
|
'endpointStatistics.top20',
|
||||||
|
'fakeScan.quality.medium',
|
||||||
'fileChooser.dragAndDrop',
|
'fileChooser.dragAndDrop',
|
||||||
'home.pipeline.title',
|
'home.pipeline.title',
|
||||||
'lang.afr',
|
'lang.afr',
|
||||||
@@ -235,6 +243,7 @@ ignore = [
|
|||||||
'pro',
|
'pro',
|
||||||
'redact.zoom',
|
'redact.zoom',
|
||||||
'sponsor',
|
'sponsor',
|
||||||
|
'team.status',
|
||||||
'text',
|
'text',
|
||||||
'validateSignature.cert.bits',
|
'validateSignature.cert.bits',
|
||||||
'validateSignature.cert.version',
|
'validateSignature.cert.version',
|
||||||
@@ -420,27 +429,17 @@ ignore = [
|
|||||||
|
|
||||||
[hu_HU]
|
[hu_HU]
|
||||||
ignore = [
|
ignore = [
|
||||||
'AddStampRequest.alphabet',
|
|
||||||
'AddStampRequest.position',
|
|
||||||
'adminUserSettings.admin',
|
|
||||||
'alphabet',
|
|
||||||
'audit.dashboard.export.json',
|
'audit.dashboard.export.json',
|
||||||
'audit.dashboard.modal.id',
|
'audit.dashboard.modal.id',
|
||||||
'audit.dashboard.table.id',
|
'audit.dashboard.table.id',
|
||||||
'certSign.name',
|
|
||||||
'cookieBanner.popUp.acceptAllBtn',
|
|
||||||
'endpointStatistics.top10',
|
'endpointStatistics.top10',
|
||||||
'endpointStatistics.top20',
|
'endpointStatistics.top20',
|
||||||
|
'home.pipeline.title',
|
||||||
'language.direction',
|
'language.direction',
|
||||||
'licenses.version',
|
'pipeline.title',
|
||||||
'poweredBy',
|
'pipelineOptions.pipelineHeader',
|
||||||
'pro',
|
'pro',
|
||||||
'sponsor',
|
'showJS.tags',
|
||||||
'text',
|
|
||||||
'validateSignature.cert.bits',
|
|
||||||
'validateSignature.cert.version',
|
|
||||||
'validateSignature.status',
|
|
||||||
'watermark.type.1',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[id_ID]
|
[id_ID]
|
||||||
@@ -495,7 +494,6 @@ ignore = [
|
|||||||
[it_IT]
|
[it_IT]
|
||||||
ignore = [
|
ignore = [
|
||||||
'lang.asm',
|
'lang.asm',
|
||||||
'lang.aze',
|
|
||||||
'lang.ceb',
|
'lang.ceb',
|
||||||
'lang.chr',
|
'lang.chr',
|
||||||
'lang.div',
|
'lang.div',
|
||||||
@@ -858,9 +856,29 @@ ignore = [
|
|||||||
|
|
||||||
[sr_LATN_RS]
|
[sr_LATN_RS]
|
||||||
ignore = [
|
ignore = [
|
||||||
|
'audit.dashboard.modal.id',
|
||||||
|
'audit.dashboard.status',
|
||||||
|
'audit.dashboard.table.id',
|
||||||
|
'endpointStatistics.top',
|
||||||
|
'endpointStatistics.top10',
|
||||||
|
'endpointStatistics.top20',
|
||||||
|
'font',
|
||||||
|
'info',
|
||||||
|
'lang.div',
|
||||||
|
'lang.epo',
|
||||||
|
'lang.hin',
|
||||||
|
'lang.iku',
|
||||||
|
'lang.mar',
|
||||||
|
'lang.san',
|
||||||
|
'lang.snd',
|
||||||
|
'lang.tel',
|
||||||
|
'lang.tgl',
|
||||||
|
'lang.urd',
|
||||||
'language.direction',
|
'language.direction',
|
||||||
'licenses.version',
|
'pro',
|
||||||
'poweredBy',
|
'showJS.tags',
|
||||||
|
'team.status',
|
||||||
|
'validateSignature.status',
|
||||||
]
|
]
|
||||||
|
|
||||||
[sv_SE]
|
[sv_SE]
|
||||||
|
|||||||
@@ -94,8 +94,14 @@ def split_photos(input_file, output_directory, tolerance=30, min_area=10000, min
|
|||||||
cropped_image = image[y:y+h, x:x+w]
|
cropped_image = image[y:y+h, x:x+w]
|
||||||
cropped_image = auto_rotate(cropped_image, angle_threshold)
|
cropped_image = auto_rotate(cropped_image, angle_threshold)
|
||||||
|
|
||||||
# Remove the added border
|
# Remove the added border, but ensure we don't create an empty image
|
||||||
cropped_image = cropped_image[border_size:-border_size, border_size:-border_size]
|
if border_size > 0 and cropped_image.shape[0] > 2 * border_size and cropped_image.shape[1] > 2 * border_size:
|
||||||
|
cropped_image = cropped_image[border_size:-border_size, border_size:-border_size]
|
||||||
|
|
||||||
|
# Check if the cropped image is valid before saving
|
||||||
|
if cropped_image.size == 0 or cropped_image.shape[0] == 0 or cropped_image.shape[1] == 0:
|
||||||
|
print(f"Warning: Skipping empty image for region {idx+1}")
|
||||||
|
continue
|
||||||
|
|
||||||
output_path = os.path.join(output_directory, f"{input_file_basename}_{idx+1}.png")
|
output_path = os.path.join(output_directory, f"{input_file_basename}_{idx+1}.png")
|
||||||
cv2.imwrite(output_path, cropped_image)
|
cv2.imwrite(output_path, cropped_image)
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ dependencies {
|
|||||||
exclude group: 'com.google.code.gson', module: 'gson'
|
exclude group: 'com.google.code.gson', module: 'gson'
|
||||||
}
|
}
|
||||||
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
|
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
|
||||||
implementation 'com.opencsv:opencsv:5.11.1' // https://mvnrepository.com/artifact/com.opencsv/opencsv
|
implementation 'com.opencsv:opencsv:5.11.2' // https://mvnrepository.com/artifact/com.opencsv/opencsv
|
||||||
|
|
||||||
// Batik
|
// Batik
|
||||||
implementation 'org.apache.xmlgraphics:batik-all:1.19'
|
implementation 'org.apache.xmlgraphics:batik-all:1.19'
|
||||||
@@ -146,5 +146,10 @@ bootJar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Configure main class for Spring Boot
|
||||||
|
springBoot {
|
||||||
|
mainClass = 'stirling.software.SPDF.SPDFApplication'
|
||||||
|
}
|
||||||
|
|
||||||
bootJar.dependsOn ':common:jar'
|
bootJar.dependsOn ':common:jar'
|
||||||
bootJar.dependsOn ':proprietary:jar'
|
bootJar.dependsOn ':proprietary:jar'
|
||||||
|
|||||||
+225
-33
@@ -21,6 +21,8 @@ public class EndpointConfiguration {
|
|||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
||||||
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
||||||
|
private Set<String> disabledGroups = new HashSet<>();
|
||||||
|
private Map<String, Set<String>> endpointAlternatives = new ConcurrentHashMap<>();
|
||||||
private final boolean runningProOrHigher;
|
private final boolean runningProOrHigher;
|
||||||
|
|
||||||
public EndpointConfiguration(
|
public EndpointConfiguration(
|
||||||
@@ -34,13 +36,14 @@ public class EndpointConfiguration {
|
|||||||
|
|
||||||
public void enableEndpoint(String endpoint) {
|
public void enableEndpoint(String endpoint) {
|
||||||
endpointStatuses.put(endpoint, true);
|
endpointStatuses.put(endpoint, true);
|
||||||
|
log.debug("Enabled endpoint: {}", endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void disableEndpoint(String endpoint) {
|
public void disableEndpoint(String endpoint) {
|
||||||
if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
|
if (!Boolean.FALSE.equals(endpointStatuses.get(endpoint))) {
|
||||||
log.debug("Disabling {}", endpoint);
|
log.debug("Disabling endpoint: {}", endpoint);
|
||||||
endpointStatuses.put(endpoint, false);
|
|
||||||
}
|
}
|
||||||
|
endpointStatuses.put(endpoint, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Boolean> getEndpointStatuses() {
|
public Map<String, Boolean> getEndpointStatuses() {
|
||||||
@@ -48,25 +51,96 @@ public class EndpointConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEndpointEnabled(String endpoint) {
|
public boolean isEndpointEnabled(String endpoint) {
|
||||||
|
String original = endpoint;
|
||||||
if (endpoint.startsWith("/")) {
|
if (endpoint.startsWith("/")) {
|
||||||
endpoint = endpoint.substring(1);
|
endpoint = endpoint.substring(1);
|
||||||
}
|
}
|
||||||
return endpointStatuses.getOrDefault(endpoint, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isGroupEnabled(String group) {
|
// Rule 1: Explicit flag wins - if disabled via disableEndpoint(), stay disabled
|
||||||
Set<String> endpoints = endpointGroups.get(group);
|
Boolean explicitStatus = endpointStatuses.get(endpoint);
|
||||||
if (endpoints == null || endpoints.isEmpty()) {
|
if (Boolean.FALSE.equals(explicitStatus)) {
|
||||||
log.debug("Group '{}' does not exist or has no endpoints", group);
|
log.debug("isEndpointEnabled('{}') -> false (explicitly disabled)", original);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String endpoint : endpoints) {
|
// Rule 2: Functional-group override - check if endpoint belongs to any disabled functional
|
||||||
if (!isEndpointEnabled(endpoint)) {
|
// group
|
||||||
|
for (String group : endpointGroups.keySet()) {
|
||||||
|
if (disabledGroups.contains(group) && endpointGroups.get(group).contains(endpoint)) {
|
||||||
|
// Skip tool groups (qpdf, OCRmyPDF, Ghostscript, LibreOffice, etc.)
|
||||||
|
if (!isToolGroup(group)) {
|
||||||
|
log.debug(
|
||||||
|
"isEndpointEnabled('{}') -> false (functional group '{}' disabled)",
|
||||||
|
original,
|
||||||
|
group);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rule 3: Tool-group fallback - check if at least one alternative tool group is enabled
|
||||||
|
Set<String> alternatives = endpointAlternatives.get(endpoint);
|
||||||
|
if (alternatives != null && !alternatives.isEmpty()) {
|
||||||
|
boolean hasEnabledToolGroup =
|
||||||
|
alternatives.stream()
|
||||||
|
.anyMatch(toolGroup -> !disabledGroups.contains(toolGroup));
|
||||||
|
log.debug(
|
||||||
|
"isEndpointEnabled('{}') -> {} (tool groups check)",
|
||||||
|
original,
|
||||||
|
hasEnabledToolGroup);
|
||||||
|
return hasEnabledToolGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rule 4: Single-dependency check - if no alternatives defined, check if endpoint belongs
|
||||||
|
// to any disabled tool groups
|
||||||
|
for (String group : endpointGroups.keySet()) {
|
||||||
|
if (isToolGroup(group)
|
||||||
|
&& disabledGroups.contains(group)
|
||||||
|
&& endpointGroups.get(group).contains(endpoint)) {
|
||||||
|
log.debug(
|
||||||
|
"isEndpointEnabled('{}') -> false (single tool group '{}' disabled, no alternatives)",
|
||||||
|
original,
|
||||||
|
group);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default: enabled if not explicitly disabled
|
||||||
|
boolean enabled = !Boolean.FALSE.equals(explicitStatus);
|
||||||
|
log.debug("isEndpointEnabled('{}') -> {} (default)", original, enabled);
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGroupEnabled(String group) {
|
||||||
|
// Rule 1: If group is explicitly disabled, it stays disabled
|
||||||
|
if (disabledGroups.contains(group)) {
|
||||||
|
log.debug("isGroupEnabled('{}') -> false (explicitly disabled)", group);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> endpoints = endpointGroups.get(group);
|
||||||
|
if (endpoints == null || endpoints.isEmpty()) {
|
||||||
|
log.debug("isGroupEnabled('{}') -> false (no endpoints)", group);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rule 2: For functional groups, check if all endpoints are enabled
|
||||||
|
// Rule 3: For tool groups, they're enabled unless explicitly disabled (handled above)
|
||||||
|
if (isToolGroup(group)) {
|
||||||
|
log.debug("isGroupEnabled('{}') -> true (tool group not disabled)", group);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For functional groups, check each endpoint individually
|
||||||
|
for (String endpoint : endpoints) {
|
||||||
|
if (!isEndpointEnabledDirectly(endpoint)) {
|
||||||
|
log.debug(
|
||||||
|
"isGroupEnabled('{}') -> false (endpoint '{}' disabled)", group, endpoint);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("isGroupEnabled('{}') -> true (all endpoints enabled)", group);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,38 +148,84 @@ public class EndpointConfiguration {
|
|||||||
endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint);
|
endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void enableGroup(String group) {
|
public void addEndpointAlternative(String endpoint, String toolGroup) {
|
||||||
Set<String> endpoints = endpointGroups.get(group);
|
endpointAlternatives.computeIfAbsent(endpoint, k -> new HashSet<>()).add(toolGroup);
|
||||||
if (endpoints != null) {
|
|
||||||
for (String endpoint : endpoints) {
|
|
||||||
enableEndpoint(endpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void disableGroup(String group) {
|
public void disableGroup(String group) {
|
||||||
Set<String> endpoints = endpointGroups.get(group);
|
if (disabledGroups.add(group)) {
|
||||||
if (endpoints != null) {
|
if (isToolGroup(group)) {
|
||||||
for (String endpoint : endpoints) {
|
log.debug(
|
||||||
disableEndpoint(endpoint);
|
"Disabling tool group: {} (endpoints with alternatives remain available)",
|
||||||
|
group);
|
||||||
|
} else {
|
||||||
|
log.debug(
|
||||||
|
"Disabling functional group: {} (will disable all endpoints in group)",
|
||||||
|
group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Only cascade to endpoints for *functional* groups
|
||||||
|
if (!isToolGroup(group)) {
|
||||||
|
Set<String> endpoints = endpointGroups.get(group);
|
||||||
|
if (endpoints != null) {
|
||||||
|
endpoints.forEach(this::disableEndpoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void enableGroup(String group) {
|
||||||
|
if (disabledGroups.remove(group)) {
|
||||||
|
log.debug("Enabling group: {}", group);
|
||||||
|
}
|
||||||
|
Set<String> endpoints = endpointGroups.get(group);
|
||||||
|
if (endpoints != null) {
|
||||||
|
endpoints.forEach(this::enableEndpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getDisabledGroups() {
|
||||||
|
return new HashSet<>(disabledGroups);
|
||||||
|
}
|
||||||
|
|
||||||
public void logDisabledEndpointsSummary() {
|
public void logDisabledEndpointsSummary() {
|
||||||
List<String> disabledList =
|
// Get all unique endpoints across all groups
|
||||||
endpointStatuses.entrySet().stream()
|
Set<String> allEndpoints =
|
||||||
.filter(entry -> !entry.getValue()) // only get disabled endpoints (value
|
endpointGroups.values().stream()
|
||||||
// is false)
|
.flatMap(Set::stream)
|
||||||
.map(Map.Entry::getKey)
|
.collect(java.util.stream.Collectors.toSet());
|
||||||
|
|
||||||
|
// Check which endpoints are actually disabled (functionally unavailable)
|
||||||
|
List<String> functionallyDisabledEndpoints =
|
||||||
|
allEndpoints.stream()
|
||||||
|
.filter(endpoint -> !isEndpointEnabled(endpoint))
|
||||||
.sorted()
|
.sorted()
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
if (!disabledList.isEmpty()) {
|
// Separate tool groups from functional groups
|
||||||
|
List<String> disabledToolGroups =
|
||||||
|
disabledGroups.stream().filter(this::isToolGroup).sorted().toList();
|
||||||
|
|
||||||
|
List<String> disabledFunctionalGroups =
|
||||||
|
disabledGroups.stream().filter(group -> !isToolGroup(group)).sorted().toList();
|
||||||
|
|
||||||
|
if (!disabledToolGroups.isEmpty()) {
|
||||||
|
log.info(
|
||||||
|
"Disabled tool groups: {} (endpoints may have alternative implementations)",
|
||||||
|
String.join(", ", disabledToolGroups));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!disabledFunctionalGroups.isEmpty()) {
|
||||||
|
log.info("Disabled functional groups: {}", String.join(", ", disabledFunctionalGroups));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!functionallyDisabledEndpoints.isEmpty()) {
|
||||||
log.info(
|
log.info(
|
||||||
"Total disabled endpoints: {}. Disabled endpoints: {}",
|
"Total disabled endpoints: {}. Disabled endpoints: {}",
|
||||||
disabledList.size(),
|
functionallyDisabledEndpoints.size(),
|
||||||
String.join(", ", disabledList));
|
String.join(", ", functionallyDisabledEndpoints));
|
||||||
|
} else if (!disabledToolGroups.isEmpty()) {
|
||||||
|
log.info(
|
||||||
|
"No endpoints disabled despite missing tools - fallback implementations available");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,8 +332,6 @@ public class EndpointConfiguration {
|
|||||||
// Unoconvert
|
// Unoconvert
|
||||||
addEndpointToGroup("Unoconvert", "file-to-pdf");
|
addEndpointToGroup("Unoconvert", "file-to-pdf");
|
||||||
|
|
||||||
addEndpointToGroup("tesseract", "ocr-pdf");
|
|
||||||
|
|
||||||
// Java
|
// Java
|
||||||
addEndpointToGroup("Java", "merge-pdfs");
|
addEndpointToGroup("Java", "merge-pdfs");
|
||||||
addEndpointToGroup("Java", "remove-pages");
|
addEndpointToGroup("Java", "remove-pages");
|
||||||
@@ -254,6 +372,7 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Java", "remove-image-pdf");
|
addEndpointToGroup("Java", "remove-image-pdf");
|
||||||
addEndpointToGroup("Java", "pdf-to-markdown");
|
addEndpointToGroup("Java", "pdf-to-markdown");
|
||||||
addEndpointToGroup("Java", "add-attachments");
|
addEndpointToGroup("Java", "add-attachments");
|
||||||
|
addEndpointToGroup("Java", "compress-pdf");
|
||||||
|
|
||||||
// Javascript
|
// Javascript
|
||||||
addEndpointToGroup("Javascript", "pdf-organizer");
|
addEndpointToGroup("Javascript", "pdf-organizer");
|
||||||
@@ -261,8 +380,42 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Javascript", "compare");
|
addEndpointToGroup("Javascript", "compare");
|
||||||
addEndpointToGroup("Javascript", "adjust-contrast");
|
addEndpointToGroup("Javascript", "adjust-contrast");
|
||||||
|
|
||||||
// qpdf dependent endpoints
|
/* qpdf */
|
||||||
addEndpointToGroup("qpdf", "repair");
|
addEndpointToGroup("qpdf", "repair");
|
||||||
|
addEndpointToGroup("qpdf", "compress-pdf");
|
||||||
|
|
||||||
|
/* Ghostscript */
|
||||||
|
addEndpointToGroup("Ghostscript", "repair");
|
||||||
|
addEndpointToGroup("Ghostscript", "compress-pdf");
|
||||||
|
|
||||||
|
/* tesseract */
|
||||||
|
addEndpointToGroup("tesseract", "ocr-pdf");
|
||||||
|
|
||||||
|
/* OCRmyPDF */
|
||||||
|
addEndpointToGroup("OCRmyPDF", "ocr-pdf");
|
||||||
|
|
||||||
|
// Multi-tool endpoints - endpoints that can be handled by multiple tools
|
||||||
|
addEndpointAlternative("repair", "qpdf");
|
||||||
|
addEndpointAlternative("repair", "Ghostscript");
|
||||||
|
addEndpointAlternative("compress-pdf", "qpdf");
|
||||||
|
addEndpointAlternative("compress-pdf", "Ghostscript");
|
||||||
|
addEndpointAlternative("compress-pdf", "Java");
|
||||||
|
addEndpointAlternative("ocr-pdf", "tesseract");
|
||||||
|
addEndpointAlternative("ocr-pdf", "OCRmyPDF");
|
||||||
|
|
||||||
|
// file-to-pdf has multiple implementations
|
||||||
|
addEndpointAlternative("file-to-pdf", "LibreOffice");
|
||||||
|
addEndpointAlternative("file-to-pdf", "Python");
|
||||||
|
addEndpointAlternative("file-to-pdf", "Unoconvert");
|
||||||
|
|
||||||
|
// pdf-to-html and pdf-to-markdown can use either LibreOffice or Pdftohtml
|
||||||
|
addEndpointAlternative("pdf-to-html", "LibreOffice");
|
||||||
|
addEndpointAlternative("pdf-to-html", "Pdftohtml");
|
||||||
|
addEndpointAlternative("pdf-to-markdown", "Pdftohtml");
|
||||||
|
|
||||||
|
// markdown-to-pdf can use either Weasyprint or Java
|
||||||
|
addEndpointAlternative("markdown-to-pdf", "Weasyprint");
|
||||||
|
addEndpointAlternative("markdown-to-pdf", "Java");
|
||||||
|
|
||||||
// Weasyprint dependent endpoints
|
// Weasyprint dependent endpoints
|
||||||
addEndpointToGroup("Weasyprint", "html-to-pdf");
|
addEndpointToGroup("Weasyprint", "html-to-pdf");
|
||||||
@@ -304,4 +457,43 @@ public class EndpointConfiguration {
|
|||||||
public Set<String> getEndpointsForGroup(String group) {
|
public Set<String> getEndpointsForGroup(String group) {
|
||||||
return endpointGroups.getOrDefault(group, new HashSet<>());
|
return endpointGroups.getOrDefault(group, new HashSet<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isToolGroup(String group) {
|
||||||
|
return "qpdf".equals(group)
|
||||||
|
|| "OCRmyPDF".equals(group)
|
||||||
|
|| "Ghostscript".equals(group)
|
||||||
|
|| "LibreOffice".equals(group)
|
||||||
|
|| "tesseract".equals(group)
|
||||||
|
|| "CLI".equals(group)
|
||||||
|
|| "Python".equals(group)
|
||||||
|
|| "OpenCV".equals(group)
|
||||||
|
|| "Unoconvert".equals(group)
|
||||||
|
|| "Java".equals(group)
|
||||||
|
|| "Javascript".equals(group)
|
||||||
|
|| "Weasyprint".equals(group)
|
||||||
|
|| "Pdftohtml".equals(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isEndpointEnabledDirectly(String endpoint) {
|
||||||
|
if (endpoint.startsWith("/")) {
|
||||||
|
endpoint = endpoint.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check explicit disable flag
|
||||||
|
Boolean explicitStatus = endpointStatuses.get(endpoint);
|
||||||
|
if (Boolean.FALSE.equals(explicitStatus)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if endpoint belongs to any disabled functional group
|
||||||
|
for (String group : endpointGroups.keySet()) {
|
||||||
|
if (disabledGroups.contains(group) && endpointGroups.get(group).contains(endpoint)) {
|
||||||
|
if (!isToolGroup(group)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ public class ExternalAppDepConfig {
|
|||||||
new HashMap<>() {
|
new HashMap<>() {
|
||||||
|
|
||||||
{
|
{
|
||||||
|
put("gs", List.of("Ghostscript"));
|
||||||
|
put("ocrmypdf", List.of("OCRmyPDF"));
|
||||||
put("soffice", List.of("LibreOffice"));
|
put("soffice", List.of("LibreOffice"));
|
||||||
put(weasyprintPath, List.of("Weasyprint"));
|
put(weasyprintPath, List.of("Weasyprint"));
|
||||||
put("pdftohtml", List.of("Pdftohtml"));
|
put("pdftohtml", List.of("Pdftohtml"));
|
||||||
@@ -109,6 +111,8 @@ public class ExternalAppDepConfig {
|
|||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void checkDependencies() {
|
public void checkDependencies() {
|
||||||
// Check core dependencies
|
// Check core dependencies
|
||||||
|
checkDependencyAndDisableGroup("gs");
|
||||||
|
checkDependencyAndDisableGroup("ocrmypdf");
|
||||||
checkDependencyAndDisableGroup("tesseract");
|
checkDependencyAndDisableGroup("tesseract");
|
||||||
checkDependencyAndDisableGroup("soffice");
|
checkDependencyAndDisableGroup("soffice");
|
||||||
checkDependencyAndDisableGroup("qpdf");
|
checkDependencyAndDisableGroup("qpdf");
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
registry.addResourceHandler("/**")
|
registry.addResourceHandler("/**")
|
||||||
.addResourceLocations(
|
.addResourceLocations(
|
||||||
"file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/");
|
"file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/");
|
||||||
registry.addResourceHandler("/js/**").addResourceLocations("classpath:/static/js/");
|
|
||||||
registry.addResourceHandler("/css/**").addResourceLocations("classpath:/static/css/");
|
|
||||||
// .setCachePeriod(0); // Optional: disable caching
|
// .setCachePeriod(0); // Optional: disable caching
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-4
@@ -35,7 +35,9 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
|
import stirling.software.common.util.PdfErrorUtils;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -146,7 +148,7 @@ public class MergeController {
|
|||||||
try (PDDocument doc = pdfDocumentFactory.load(file)) {
|
try (PDDocument doc = pdfDocumentFactory.load(file)) {
|
||||||
pageIndex += doc.getNumberOfPages();
|
pageIndex += doc.getNumberOfPages();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Error loading document for TOC generation", e);
|
ExceptionUtils.logException("document loading for TOC generation", e);
|
||||||
pageIndex++; // Increment by at least one if we can't determine page count
|
pageIndex++; // Increment by at least one if we can't determine page count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,8 +191,17 @@ public class MergeController {
|
|||||||
mergedTempFile = Files.createTempFile("merged-", ".pdf").toFile();
|
mergedTempFile = Files.createTempFile("merged-", ".pdf").toFile();
|
||||||
mergerUtility.setDestinationFileName(mergedTempFile.getAbsolutePath());
|
mergerUtility.setDestinationFileName(mergedTempFile.getAbsolutePath());
|
||||||
|
|
||||||
mergerUtility.mergeDocuments(
|
try {
|
||||||
pdfDocumentFactory.getStreamCacheFunction(totalSize)); // Merge the documents
|
mergerUtility.mergeDocuments(
|
||||||
|
pdfDocumentFactory.getStreamCacheFunction(
|
||||||
|
totalSize)); // Merge the documents
|
||||||
|
} catch (IOException e) {
|
||||||
|
ExceptionUtils.logException("PDF merge", e);
|
||||||
|
if (PdfErrorUtils.isCorruptedPdfError(e)) {
|
||||||
|
throw ExceptionUtils.createMultiplePdfCorruptedException(e);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
// Load the merged PDF document
|
// Load the merged PDF document
|
||||||
mergedDocument = pdfDocumentFactory.load(mergedTempFile);
|
mergedDocument = pdfDocumentFactory.load(mergedTempFile);
|
||||||
@@ -229,7 +240,11 @@ public class MergeController {
|
|||||||
baos, mergedFileName); // Return the modified PDF
|
baos, mergedFileName); // Return the modified PDF
|
||||||
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.error("Error in merge pdf process", ex);
|
if (ex instanceof IOException && PdfErrorUtils.isCorruptedPdfError((IOException) ex)) {
|
||||||
|
log.warn("Corrupted PDF detected in merge pdf process: {}", ex.getMessage());
|
||||||
|
} else {
|
||||||
|
log.error("Error in merge pdf process", ex);
|
||||||
|
}
|
||||||
throw ex;
|
throw ex;
|
||||||
} finally {
|
} finally {
|
||||||
if (mergedDocument != null) {
|
if (mergedDocument != null) {
|
||||||
|
|||||||
+3
-2
@@ -25,6 +25,7 @@ import stirling.software.SPDF.model.SortTypes;
|
|||||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
|
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@@ -288,8 +289,8 @@ public class RearrangePagesPDFController {
|
|||||||
.replaceFirst("[.][^.]+$", "")
|
.replaceFirst("[.][^.]+$", "")
|
||||||
+ "_rearranged.pdf");
|
+ "_rearranged.pdf");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Failed rearranging documents", e);
|
ExceptionUtils.logException("document rearrangement", e);
|
||||||
return null;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
|
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -43,7 +44,8 @@ public class RotationController {
|
|||||||
|
|
||||||
// Validate the angle is a multiple of 90
|
// Validate the angle is a multiple of 90
|
||||||
if (angle % 90 != 0) {
|
if (angle % 90 != 0) {
|
||||||
throw new IllegalArgumentException("Angle must be a multiple of 90");
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.angleNotMultipleOf90", "Angle must be a multiple of 90");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the PDF document
|
// Load the PDF document
|
||||||
|
|||||||
+2
-3
@@ -27,6 +27,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.ScalePagesRequest;
|
import stirling.software.SPDF.model.api.general.ScalePagesRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -120,9 +121,7 @@ public class ScalePagesController {
|
|||||||
return sizeMap.get(targetPDRectangle);
|
return sizeMap.get(targetPDRectangle);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new IllegalArgumentException(
|
throw ExceptionUtils.createInvalidPageSizeException(targetPDRectangle);
|
||||||
"Invalid PDRectangle. It must be one of the following: A0, A1, A2, A3, A4, A5, A6,"
|
|
||||||
+ " LETTER, LEGAL, KEEP");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, PDRectangle> getSizeMap() {
|
private Map<String, PDRectangle> getSizeMap() {
|
||||||
|
|||||||
+6
-5
@@ -29,6 +29,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
|
|
||||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -71,7 +72,7 @@ public class SplitPDFController {
|
|||||||
pageNumbers.add(totalPages - 1);
|
pageNumbers.add(totalPages - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(
|
log.debug(
|
||||||
"Splitting PDF into pages: {}",
|
"Splitting PDF into pages: {}",
|
||||||
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
|
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ public class SplitPDFController {
|
|||||||
for (int i = previousPageNumber; i <= splitPoint; i++) {
|
for (int i = previousPageNumber; i <= splitPoint; i++) {
|
||||||
PDPage page = document.getPage(i);
|
PDPage page = document.getPage(i);
|
||||||
splitDocument.addPage(page);
|
splitDocument.addPage(page);
|
||||||
log.info("Adding page {} to split document", i);
|
log.debug("Adding page {} to split document", i);
|
||||||
}
|
}
|
||||||
previousPageNumber = splitPoint + 1;
|
previousPageNumber = splitPoint + 1;
|
||||||
|
|
||||||
@@ -96,7 +97,7 @@ public class SplitPDFController {
|
|||||||
|
|
||||||
splitDocumentsBoas.add(baos);
|
splitDocumentsBoas.add(baos);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed splitting documents and saving them", e);
|
ExceptionUtils.logException("document splitting and saving", e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,14 +123,14 @@ public class SplitPDFController {
|
|||||||
zipOut.write(pdf);
|
zipOut.write(pdf);
|
||||||
zipOut.closeEntry();
|
zipOut.closeEntry();
|
||||||
|
|
||||||
log.info("Wrote split document {} to zip file", fileName);
|
log.debug("Wrote split document {} to zip file", fileName);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed writing to zip", e);
|
log.error("Failed writing to zip", e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Successfully created zip file with split documents: {}", zipFile.toString());
|
log.debug("Successfully created zip file with split documents: {}", zipFile.toString());
|
||||||
byte[] data = Files.readAllBytes(zipFile);
|
byte[] data = Files.readAllBytes(zipFile);
|
||||||
Files.deleteIfExists(zipFile);
|
Files.deleteIfExists(zipFile);
|
||||||
|
|
||||||
|
|||||||
+9
-6
@@ -35,6 +35,7 @@ import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
|
|||||||
import stirling.software.common.model.PdfMetadata;
|
import stirling.software.common.model.PdfMetadata;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.service.PdfMetadataService;
|
import stirling.software.common.service.PdfMetadataService;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -131,7 +132,8 @@ public class SplitPdfByChaptersController {
|
|||||||
Integer bookmarkLevel =
|
Integer bookmarkLevel =
|
||||||
request.getBookmarkLevel(); // levels start from 0 (top most bookmarks)
|
request.getBookmarkLevel(); // levels start from 0 (top most bookmarks)
|
||||||
if (bookmarkLevel < 0) {
|
if (bookmarkLevel < 0) {
|
||||||
throw new IllegalArgumentException("Invalid bookmark level");
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.invalidArgument", "Invalid argument: {0}", "bookmark level");
|
||||||
}
|
}
|
||||||
sourceDocument = pdfDocumentFactory.load(file);
|
sourceDocument = pdfDocumentFactory.load(file);
|
||||||
|
|
||||||
@@ -139,7 +141,8 @@ public class SplitPdfByChaptersController {
|
|||||||
|
|
||||||
if (outline == null) {
|
if (outline == null) {
|
||||||
log.warn("No outline found for {}", file.getOriginalFilename());
|
log.warn("No outline found for {}", file.getOriginalFilename());
|
||||||
throw new IllegalArgumentException("No outline found");
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.pdfBookmarksNotFound", "No PDF bookmarks/outline found in document");
|
||||||
}
|
}
|
||||||
List<Bookmark> bookmarks = new ArrayList<>();
|
List<Bookmark> bookmarks = new ArrayList<>();
|
||||||
try {
|
try {
|
||||||
@@ -156,7 +159,7 @@ public class SplitPdfByChaptersController {
|
|||||||
Bookmark lastBookmark = bookmarks.get(bookmarks.size() - 1);
|
Bookmark lastBookmark = bookmarks.get(bookmarks.size() - 1);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Unable to extract outline items", e);
|
ExceptionUtils.logException("outline extraction", e);
|
||||||
return ResponseEntity.internalServerError()
|
return ResponseEntity.internalServerError()
|
||||||
.body("Unable to extract outline items".getBytes());
|
.body("Unable to extract outline items".getBytes());
|
||||||
}
|
}
|
||||||
@@ -252,7 +255,7 @@ public class SplitPdfByChaptersController {
|
|||||||
zipOut.write(pdf);
|
zipOut.write(pdf);
|
||||||
zipOut.closeEntry();
|
zipOut.closeEntry();
|
||||||
|
|
||||||
log.info("Wrote split document {} to zip file", fileName);
|
log.debug("Wrote split document {} to zip file", fileName);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed writing to zip", e);
|
log.error("Failed writing to zip", e);
|
||||||
@@ -280,7 +283,7 @@ public class SplitPdfByChaptersController {
|
|||||||
i++) {
|
i++) {
|
||||||
PDPage page = sourceDocument.getPage(i);
|
PDPage page = sourceDocument.getPage(i);
|
||||||
splitDocument.addPage(page);
|
splitDocument.addPage(page);
|
||||||
log.info("Adding page {} to split document", i);
|
log.debug("Adding page {} to split document", i);
|
||||||
}
|
}
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
if (includeMetadata) {
|
if (includeMetadata) {
|
||||||
@@ -291,7 +294,7 @@ public class SplitPdfByChaptersController {
|
|||||||
|
|
||||||
splitDocumentsBoas.add(baos);
|
splitDocumentsBoas.add(baos);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed splitting documents and saving them", e);
|
ExceptionUtils.logException("document splitting and saving", e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-14
@@ -26,6 +26,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
|
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@@ -96,13 +97,15 @@ public class SplitPdfBySizeController {
|
|||||||
handleSplitByDocCount(sourceDocument, documentCount, zipOut, filename);
|
handleSplitByDocCount(sourceDocument, documentCount, zipOut, filename);
|
||||||
} else {
|
} else {
|
||||||
log.error("Invalid split type: {}", type);
|
log.error("Invalid split type: {}", type);
|
||||||
throw new IllegalArgumentException(
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
"Invalid argument for split type: " + type);
|
"error.invalidArgument",
|
||||||
|
"Invalid argument: {0}",
|
||||||
|
"split type: " + type);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("PDF splitting completed successfully");
|
log.debug("PDF splitting completed successfully");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error loading or processing PDF document", e);
|
ExceptionUtils.logException("PDF document loading or processing", e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -111,7 +114,7 @@ public class SplitPdfBySizeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Exception during PDF splitting process", e);
|
ExceptionUtils.logException("PDF splitting process", e);
|
||||||
throw e; // Re-throw to ensure proper error response
|
throw e; // Re-throw to ensure proper error response
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
@@ -275,8 +278,8 @@ public class SplitPdfBySizeController {
|
|||||||
currentDoc = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
|
currentDoc = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
|
||||||
log.debug("Successfully created initial output document");
|
log.debug("Successfully created initial output document");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error creating initial output document", e);
|
ExceptionUtils.logException("initial output document creation", e);
|
||||||
throw new IOException("Failed to create initial output document", e);
|
throw ExceptionUtils.createFileProcessingException("split", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
int fileIndex = 1;
|
int fileIndex = 1;
|
||||||
@@ -295,7 +298,7 @@ public class SplitPdfBySizeController {
|
|||||||
log.debug("Successfully added page {} to current document", pageIndex);
|
log.debug("Successfully added page {} to current document", pageIndex);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error adding page {} to current document", pageIndex, e);
|
log.error("Error adding page {} to current document", pageIndex, e);
|
||||||
throw new IOException("Failed to add page to document", e);
|
throw ExceptionUtils.createFileProcessingException("split", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPageCount++;
|
currentPageCount++;
|
||||||
@@ -320,7 +323,7 @@ public class SplitPdfBySizeController {
|
|||||||
log.debug("Successfully created new document");
|
log.debug("Successfully created new document");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error creating new document for next part", e);
|
log.error("Error creating new document for next part", e);
|
||||||
throw new IOException("Failed to create new document", e);
|
throw ExceptionUtils.createFileProcessingException("split", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPageCount = 0;
|
currentPageCount = 0;
|
||||||
@@ -329,7 +332,7 @@ public class SplitPdfBySizeController {
|
|||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error iterating through pages", e);
|
log.error("Error iterating through pages", e);
|
||||||
throw new IOException("Failed to iterate through pages", e);
|
throw ExceptionUtils.createFileProcessingException("split", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the last document if it contains any pages
|
// Add the last document if it contains any pages
|
||||||
@@ -351,7 +354,7 @@ public class SplitPdfBySizeController {
|
|||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error checking or saving final document", e);
|
log.error("Error checking or saving final document", e);
|
||||||
throw new IOException("Failed to process final document", e);
|
throw ExceptionUtils.createFileProcessingException("split", e);
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
log.debug("Closing final document");
|
log.debug("Closing final document");
|
||||||
@@ -390,7 +393,7 @@ public class SplitPdfBySizeController {
|
|||||||
log.debug("Successfully created document {} of {}", i + 1, documentCount);
|
log.debug("Successfully created document {} of {}", i + 1, documentCount);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error creating document {} of {}", i + 1, documentCount, e);
|
log.error("Error creating document {} of {}", i + 1, documentCount, e);
|
||||||
throw new IOException("Failed to create document", e);
|
throw ExceptionUtils.createFileProcessingException("split", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
int pagesToAdd = pagesPerDocument + (i < extraPages ? 1 : 0);
|
int pagesToAdd = pagesPerDocument + (i < extraPages ? 1 : 0);
|
||||||
@@ -408,7 +411,7 @@ public class SplitPdfBySizeController {
|
|||||||
currentPageIndex++;
|
currentPageIndex++;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error adding page {} to document {}", j + 1, i + 1, e);
|
log.error("Error adding page {} to document {}", j + 1, i + 1, e);
|
||||||
throw new IOException("Failed to add page to document", e);
|
throw ExceptionUtils.createFileProcessingException("split", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +440,7 @@ public class SplitPdfBySizeController {
|
|||||||
log.debug("Successfully saved document part {} ({} bytes)", index, outStream.size());
|
log.debug("Successfully saved document part {} ({} bytes)", index, outStream.size());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error saving document part {} to byte array", index, e);
|
log.error("Error saving document part {} to byte array", index, e);
|
||||||
throw new IOException("Failed to save document to byte array", e);
|
throw ExceptionUtils.createFileProcessingException("split", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -465,7 +468,7 @@ public class SplitPdfBySizeController {
|
|||||||
log.debug("Successfully added document part {} to ZIP", index);
|
log.debug("Successfully added document part {} to ZIP", index);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error adding document part {} to ZIP", index, e);
|
log.error("Error adding document part {} to ZIP", index, e);
|
||||||
throw new IOException("Failed to add document to ZIP file", e);
|
throw ExceptionUtils.createFileProcessingException("split", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -70,17 +70,17 @@ public class ToSinglePageController {
|
|||||||
float yOffset = totalHeight;
|
float yOffset = totalHeight;
|
||||||
|
|
||||||
// For each page, copy its content to the new page at the correct offset
|
// For each page, copy its content to the new page at the correct offset
|
||||||
|
int pageIndex = 0;
|
||||||
for (PDPage page : sourceDocument.getPages()) {
|
for (PDPage page : sourceDocument.getPages()) {
|
||||||
PDFormXObject form =
|
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, pageIndex);
|
||||||
layerUtility.importPageAsForm(
|
|
||||||
sourceDocument, sourceDocument.getPages().indexOf(page));
|
|
||||||
AffineTransform af =
|
AffineTransform af =
|
||||||
AffineTransform.getTranslateInstance(
|
AffineTransform.getTranslateInstance(
|
||||||
0, yOffset - page.getMediaBox().getHeight());
|
0, yOffset - page.getMediaBox().getHeight());
|
||||||
layerUtility.wrapInSaveRestore(newPage);
|
layerUtility.wrapInSaveRestore(newPage);
|
||||||
String defaultLayerName = "Layer" + sourceDocument.getPages().indexOf(page);
|
String defaultLayerName = "Layer" + pageIndex;
|
||||||
layerUtility.appendFormAsLayer(newPage, form, af, defaultLayerName);
|
layerUtility.appendFormAsLayer(newPage, form, af, defaultLayerName);
|
||||||
yOffset -= page.getMediaBox().getHeight();
|
yOffset -= page.getMediaBox().getHeight();
|
||||||
|
pageIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
|||||||
+4
-3
@@ -17,6 +17,7 @@ import stirling.software.common.configuration.RuntimePathConfig;
|
|||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
|
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.FileToPdf;
|
import stirling.software.common.util.FileToPdf;
|
||||||
import stirling.software.common.util.TempFileManager;
|
import stirling.software.common.util.TempFileManager;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
@@ -46,14 +47,14 @@ public class ConvertHtmlToPDF {
|
|||||||
MultipartFile fileInput = request.getFileInput();
|
MultipartFile fileInput = request.getFileInput();
|
||||||
|
|
||||||
if (fileInput == null) {
|
if (fileInput == null) {
|
||||||
throw new IllegalArgumentException(
|
throw ExceptionUtils.createHtmlFileRequiredException();
|
||||||
"Please provide an HTML or ZIP file for conversion.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String originalFilename = Filenames.toSimpleFileName(fileInput.getOriginalFilename());
|
String originalFilename = Filenames.toSimpleFileName(fileInput.getOriginalFilename());
|
||||||
if (originalFilename == null
|
if (originalFilename == null
|
||||||
|| (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) {
|
|| (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) {
|
||||||
throw new IllegalArgumentException("File must be either .html or .zip format.");
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.fileFormatRequired", "File must be in {0} format", ".html or .zip");
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean disableSanitize =
|
boolean disableSanitize =
|
||||||
|
|||||||
+2
-1
@@ -34,6 +34,7 @@ import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
|
|||||||
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.CheckProgramInstall;
|
import stirling.software.common.util.CheckProgramInstall;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
import stirling.software.common.util.PdfUtils;
|
import stirling.software.common.util.PdfUtils;
|
||||||
import stirling.software.common.util.ProcessExecutor;
|
import stirling.software.common.util.ProcessExecutor;
|
||||||
@@ -104,7 +105,7 @@ public class ConvertImgPDFController {
|
|||||||
log.error("resultant bytes for {} is null, error converting ", filename);
|
log.error("resultant bytes for {} is null, error converting ", filename);
|
||||||
}
|
}
|
||||||
if ("webp".equalsIgnoreCase(imageFormat) && !CheckProgramInstall.isPythonAvailable()) {
|
if ("webp".equalsIgnoreCase(imageFormat) && !CheckProgramInstall.isPythonAvailable()) {
|
||||||
throw new IOException("Python is not installed. Required for WebP conversion.");
|
throw ExceptionUtils.createPythonRequiredForWebpException();
|
||||||
} else if ("webp".equalsIgnoreCase(imageFormat)
|
} else if ("webp".equalsIgnoreCase(imageFormat)
|
||||||
&& CheckProgramInstall.isPythonAvailable()) {
|
&& CheckProgramInstall.isPythonAvailable()) {
|
||||||
// Write the output stream to a temp file
|
// Write the output stream to a temp file
|
||||||
|
|||||||
+5
-2
@@ -27,6 +27,7 @@ import stirling.software.common.configuration.RuntimePathConfig;
|
|||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.model.api.GeneralFile;
|
import stirling.software.common.model.api.GeneralFile;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.FileToPdf;
|
import stirling.software.common.util.FileToPdf;
|
||||||
import stirling.software.common.util.TempFileManager;
|
import stirling.software.common.util.TempFileManager;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
@@ -55,12 +56,14 @@ public class ConvertMarkdownToPdf {
|
|||||||
MultipartFile fileInput = generalFile.getFileInput();
|
MultipartFile fileInput = generalFile.getFileInput();
|
||||||
|
|
||||||
if (fileInput == null) {
|
if (fileInput == null) {
|
||||||
throw new IllegalArgumentException("Please provide a Markdown file for conversion.");
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.fileFormatRequired", "File must be in {0} format", "Markdown");
|
||||||
}
|
}
|
||||||
|
|
||||||
String originalFilename = Filenames.toSimpleFileName(fileInput.getOriginalFilename());
|
String originalFilename = Filenames.toSimpleFileName(fileInput.getOriginalFilename());
|
||||||
if (originalFilename == null || !originalFilename.endsWith(".md")) {
|
if (originalFilename == null || !originalFilename.endsWith(".md")) {
|
||||||
throw new IllegalArgumentException("File must be in .md format.");
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.fileFormatRequired", "File must be in {0} format", ".md");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert Markdown to HTML using CommonMark
|
// Convert Markdown to HTML using CommonMark
|
||||||
|
|||||||
+4
-5
@@ -67,6 +67,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.converters.PdfToPdfARequest;
|
import stirling.software.SPDF.model.api.converters.PdfToPdfARequest;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.ProcessExecutor;
|
import stirling.software.common.util.ProcessExecutor;
|
||||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
@@ -90,7 +91,7 @@ public class ConvertPDFToPDFA {
|
|||||||
// Validate input file type
|
// Validate input file type
|
||||||
if (!"application/pdf".equals(inputFile.getContentType())) {
|
if (!"application/pdf".equals(inputFile.getContentType())) {
|
||||||
log.error("Invalid input file type: {}", inputFile.getContentType());
|
log.error("Invalid input file type: {}", inputFile.getContentType());
|
||||||
throw new IllegalArgumentException("Input file must be a PDF");
|
throw ExceptionUtils.createPdfFileRequiredException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the original filename without extension
|
// Get the original filename without extension
|
||||||
@@ -241,15 +242,13 @@ public class ConvertPDFToPDFA {
|
|||||||
|
|
||||||
if (returnCode.getRc() != 0) {
|
if (returnCode.getRc() != 0) {
|
||||||
log.error("PDF/A conversion failed with return code: {}", returnCode.getRc());
|
log.error("PDF/A conversion failed with return code: {}", returnCode.getRc());
|
||||||
throw new RuntimeException("PDF/A conversion failed");
|
throw ExceptionUtils.createPdfaConversionFailedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the output file
|
// Get the output file
|
||||||
File[] outputFiles = tempOutputDir.toFile().listFiles();
|
File[] outputFiles = tempOutputDir.toFile().listFiles();
|
||||||
if (outputFiles == null || outputFiles.length != 1) {
|
if (outputFiles == null || outputFiles.length != 1) {
|
||||||
throw new RuntimeException(
|
throw ExceptionUtils.createPdfaConversionFailedException();
|
||||||
"Expected one output PDF, found "
|
|
||||||
+ (outputFiles == null ? "none" : outputFiles.length));
|
|
||||||
}
|
}
|
||||||
return outputFiles[0].toPath();
|
return outputFiles[0].toPath();
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-3
@@ -23,6 +23,7 @@ import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
|
|||||||
import stirling.software.common.configuration.RuntimePathConfig;
|
import stirling.software.common.configuration.RuntimePathConfig;
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
import stirling.software.common.util.ProcessExecutor;
|
import stirling.software.common.util.ProcessExecutor;
|
||||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||||
@@ -50,16 +51,19 @@ public class ConvertWebsiteToPDF {
|
|||||||
String URL = request.getUrlInput();
|
String URL = request.getUrlInput();
|
||||||
|
|
||||||
if (!applicationProperties.getSystem().getEnableUrlToPDF()) {
|
if (!applicationProperties.getSystem().getEnableUrlToPDF()) {
|
||||||
throw new IllegalArgumentException("This endpoint has been disabled by the admin.");
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.endpointDisabled", "This endpoint has been disabled by the admin");
|
||||||
}
|
}
|
||||||
// Validate the URL format
|
// Validate the URL format
|
||||||
if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
|
if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
|
||||||
throw new IllegalArgumentException("Invalid URL format provided.");
|
throw ExceptionUtils.createInvalidArgumentException(
|
||||||
|
"URL", "provided format is invalid");
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate the URL is reachable
|
// validate the URL is reachable
|
||||||
if (!GeneralUtils.isURLReachable(URL)) {
|
if (!GeneralUtils.isURLReachable(URL)) {
|
||||||
throw new IllegalArgumentException("URL is not reachable, please provide a valid URL.");
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.urlNotReachable", "URL is not reachable, please provide a valid URL");
|
||||||
}
|
}
|
||||||
|
|
||||||
Path tempOutputFile = null;
|
Path tempOutputFile = null;
|
||||||
|
|||||||
+5
-4
@@ -25,6 +25,7 @@ import stirling.software.SPDF.model.api.filter.FileSizeRequest;
|
|||||||
import stirling.software.SPDF.model.api.filter.PageRotationRequest;
|
import stirling.software.SPDF.model.api.filter.PageRotationRequest;
|
||||||
import stirling.software.SPDF.model.api.filter.PageSizeRequest;
|
import stirling.software.SPDF.model.api.filter.PageSizeRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.PdfUtils;
|
import stirling.software.common.util.PdfUtils;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@@ -96,7 +97,7 @@ public class FilterController {
|
|||||||
valid = actualPageCount < pageCount;
|
valid = actualPageCount < pageCount;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||||
@@ -139,7 +140,7 @@ public class FilterController {
|
|||||||
valid = actualArea < standardArea;
|
valid = actualArea < standardArea;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||||
@@ -172,7 +173,7 @@ public class FilterController {
|
|||||||
valid = actualFileSize < fileSize;
|
valid = actualFileSize < fileSize;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||||
@@ -208,7 +209,7 @@ public class FilterController {
|
|||||||
valid = actualRotation < rotation;
|
valid = actualRotation < rotation;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||||
|
|||||||
+161
-35
@@ -47,11 +47,13 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||||
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
|
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
import stirling.software.common.util.ProcessExecutor;
|
import stirling.software.common.util.ProcessExecutor;
|
||||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||||
@@ -61,16 +63,18 @@ import stirling.software.common.util.WebResponseUtils;
|
|||||||
@RequestMapping("/api/v1/misc")
|
@RequestMapping("/api/v1/misc")
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class CompressController {
|
public class CompressController {
|
||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
private final boolean qpdfEnabled;
|
private final EndpointConfiguration endpointConfiguration;
|
||||||
|
|
||||||
public CompressController(
|
private boolean isQpdfEnabled() {
|
||||||
CustomPDFDocumentFactory pdfDocumentFactory,
|
return endpointConfiguration.isGroupEnabled("qpdf");
|
||||||
EndpointConfiguration endpointConfiguration) {
|
}
|
||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
|
||||||
this.qpdfEnabled = endpointConfiguration.isGroupEnabled("qpdf");
|
private boolean isGhostscriptEnabled() {
|
||||||
|
return endpointConfiguration.isGroupEnabled("Ghostscript");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -597,12 +601,12 @@ public class CompressController {
|
|||||||
if (bytesRead > 0) {
|
if (bytesRead > 0) {
|
||||||
byte[] dataToHash =
|
byte[] dataToHash =
|
||||||
bytesRead == buffer.length ? buffer : Arrays.copyOf(buffer, bytesRead);
|
bytesRead == buffer.length ? buffer : Arrays.copyOf(buffer, bytesRead);
|
||||||
return bytesToHexString(generatMD5(dataToHash));
|
return bytesToHexString(generateMD5(dataToHash));
|
||||||
}
|
}
|
||||||
return "empty-stream";
|
return "empty-stream";
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error generating image hash", e);
|
ExceptionUtils.logException("image hash generation", e);
|
||||||
return "fallback-" + System.identityHashCode(image);
|
return "fallback-" + System.identityHashCode(image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -615,12 +619,12 @@ public class CompressController {
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] generatMD5(byte[] data) throws IOException {
|
private byte[] generateMD5(byte[] data) throws IOException {
|
||||||
try {
|
try {
|
||||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||||
return md.digest(data); // Get the MD5 hash of the image bytes
|
return md.digest(data); // Get the MD5 hash of the image bytes
|
||||||
} catch (NoSuchAlgorithmException e) {
|
} catch (NoSuchAlgorithmException e) {
|
||||||
throw new RuntimeException("MD5 algorithm not available", e);
|
throw ExceptionUtils.createMd5AlgorithmException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -697,25 +701,69 @@ public class CompressController {
|
|||||||
|
|
||||||
boolean sizeMet = false;
|
boolean sizeMet = false;
|
||||||
boolean imageCompressionApplied = false;
|
boolean imageCompressionApplied = false;
|
||||||
boolean qpdfCompressionApplied = false;
|
boolean externalCompressionApplied = false;
|
||||||
|
|
||||||
if (qpdfEnabled && optimizeLevel <= 3) {
|
|
||||||
optimizeLevel = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (!sizeMet && optimizeLevel <= 9) {
|
while (!sizeMet && optimizeLevel <= 9) {
|
||||||
// Apply image compression for levels 4-9
|
// Apply external compression first
|
||||||
if ((optimizeLevel >= 3 || Boolean.TRUE.equals(convertToGrayscale))
|
if (!externalCompressionApplied) {
|
||||||
&& !imageCompressionApplied) {
|
boolean ghostscriptSuccess = false;
|
||||||
double scaleFactor = getScaleFactorForLevel(optimizeLevel);
|
|
||||||
float jpegQuality = getJpegQualityForLevel(optimizeLevel);
|
|
||||||
|
|
||||||
// Compress images
|
// Try Ghostscript first if available - for ANY compression level
|
||||||
|
if (isGhostscriptEnabled()) {
|
||||||
|
try {
|
||||||
|
applyGhostscriptCompression(
|
||||||
|
request, optimizeLevel, currentFile, tempFiles);
|
||||||
|
log.info("Ghostscript compression applied successfully");
|
||||||
|
ghostscriptSuccess = true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Ghostscript compression failed, trying fallback methods");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to QPDF if Ghostscript failed or not available (levels 1-3 only)
|
||||||
|
if (!ghostscriptSuccess && isQpdfEnabled() && optimizeLevel <= 3) {
|
||||||
|
try {
|
||||||
|
applyQpdfCompression(request, optimizeLevel, currentFile, tempFiles);
|
||||||
|
log.info("QPDF compression applied successfully");
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("QPDF compression also failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ghostscriptSuccess && !isQpdfEnabled()) {
|
||||||
|
log.info(
|
||||||
|
"No external compression tools available, using image compression only");
|
||||||
|
}
|
||||||
|
|
||||||
|
externalCompressionApplied = true;
|
||||||
|
|
||||||
|
// Skip image compression if Ghostscript succeeded
|
||||||
|
if (ghostscriptSuccess) {
|
||||||
|
imageCompressionApplied = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply image compression for levels 4+ only if Ghostscript didn't run
|
||||||
|
if ((optimizeLevel >= 4 || Boolean.TRUE.equals(convertToGrayscale))
|
||||||
|
&& !imageCompressionApplied) {
|
||||||
|
// Use different scale factors based on level
|
||||||
|
double scaleFactor =
|
||||||
|
switch (optimizeLevel) {
|
||||||
|
case 4 -> 0.95; // 95% of original size
|
||||||
|
case 5 -> 0.9; // 90% of original size
|
||||||
|
case 6 -> 0.8; // 80% of original size
|
||||||
|
case 7 -> 0.7; // 70% of original size
|
||||||
|
case 8 -> 0.65; // 65% of original size
|
||||||
|
case 9 -> 0.5; // 50% of original size
|
||||||
|
default -> 1.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
log.info("Applying image compression with scale factor: {}", scaleFactor);
|
||||||
Path compressedImageFile =
|
Path compressedImageFile =
|
||||||
compressImagesInPDF(
|
compressImagesInPDF(
|
||||||
currentFile,
|
currentFile,
|
||||||
scaleFactor,
|
scaleFactor,
|
||||||
jpegQuality,
|
0.7f, // Default JPEG quality
|
||||||
Boolean.TRUE.equals(convertToGrayscale));
|
Boolean.TRUE.equals(convertToGrayscale));
|
||||||
|
|
||||||
tempFiles.add(compressedImageFile);
|
tempFiles.add(compressedImageFile);
|
||||||
@@ -723,18 +771,6 @@ public class CompressController {
|
|||||||
imageCompressionApplied = true;
|
imageCompressionApplied = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply QPDF compression for all levels
|
|
||||||
if (!qpdfCompressionApplied && qpdfEnabled) {
|
|
||||||
applyQpdfCompression(request, optimizeLevel, currentFile, tempFiles);
|
|
||||||
qpdfCompressionApplied = true;
|
|
||||||
} else if (!qpdfCompressionApplied) {
|
|
||||||
// If QPDF is disabled, mark as applied and log
|
|
||||||
if (!qpdfEnabled) {
|
|
||||||
log.info("Skipping QPDF compression as QPDF group is disabled");
|
|
||||||
}
|
|
||||||
qpdfCompressionApplied = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if target size reached or not in auto mode
|
// Check if target size reached or not in auto mode
|
||||||
long outputFileSize = Files.size(currentFile);
|
long outputFileSize = Files.size(currentFile);
|
||||||
if (outputFileSize <= expectedOutputSize || !autoMode) {
|
if (outputFileSize <= expectedOutputSize || !autoMode) {
|
||||||
@@ -754,7 +790,7 @@ public class CompressController {
|
|||||||
} else {
|
} else {
|
||||||
// Reset flags for next iteration with higher optimization level
|
// Reset flags for next iteration with higher optimization level
|
||||||
imageCompressionApplied = false;
|
imageCompressionApplied = false;
|
||||||
qpdfCompressionApplied = false;
|
externalCompressionApplied = false;
|
||||||
optimizeLevel = newOptimizeLevel;
|
optimizeLevel = newOptimizeLevel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -788,6 +824,96 @@ public class CompressController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run Ghostscript compression
|
||||||
|
private void applyGhostscriptCompression(
|
||||||
|
OptimizePdfRequest request, int optimizeLevel, Path currentFile, List<Path> tempFiles)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
long preGsSize = Files.size(currentFile);
|
||||||
|
log.info("Pre-Ghostscript file size: {}", GeneralUtils.formatBytes(preGsSize));
|
||||||
|
|
||||||
|
// Create output file for Ghostscript
|
||||||
|
Path gsOutputFile = Files.createTempFile("gs_output_", ".pdf");
|
||||||
|
tempFiles.add(gsOutputFile);
|
||||||
|
|
||||||
|
// Build Ghostscript command based on optimization level
|
||||||
|
List<String> command = new ArrayList<>();
|
||||||
|
command.add("gs");
|
||||||
|
command.add("-sDEVICE=pdfwrite");
|
||||||
|
command.add("-dCompatibilityLevel=1.5");
|
||||||
|
command.add("-dNOPAUSE");
|
||||||
|
command.add("-dQUIET");
|
||||||
|
command.add("-dBATCH");
|
||||||
|
|
||||||
|
// Map optimization levels to Ghostscript settings
|
||||||
|
switch (optimizeLevel) {
|
||||||
|
case 1:
|
||||||
|
command.add("-dPDFSETTINGS=/prepress");
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
command.add("-dPDFSETTINGS=/printer");
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
command.add("-dPDFSETTINGS=/ebook");
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
case 5:
|
||||||
|
command.add("-dPDFSETTINGS=/screen");
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
case 7:
|
||||||
|
command.add("-dPDFSETTINGS=/screen");
|
||||||
|
command.add("-dColorImageResolution=150");
|
||||||
|
command.add("-dGrayImageResolution=150");
|
||||||
|
command.add("-dMonoImageResolution=300");
|
||||||
|
break;
|
||||||
|
case 8:
|
||||||
|
case 9:
|
||||||
|
command.add("-dPDFSETTINGS=/screen");
|
||||||
|
command.add("-dColorImageResolution=100");
|
||||||
|
command.add("-dGrayImageResolution=100");
|
||||||
|
command.add("-dMonoImageResolution=200");
|
||||||
|
break;
|
||||||
|
case 10:
|
||||||
|
command.add("-dPDFSETTINGS=/screen");
|
||||||
|
command.add("-dColorImageResolution=72");
|
||||||
|
command.add("-dGrayImageResolution=72");
|
||||||
|
command.add("-dMonoImageResolution=150");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
command.add("-dPDFSETTINGS=/screen");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
command.add("-sOutputFile=" + gsOutputFile.toString());
|
||||||
|
command.add(currentFile.toString());
|
||||||
|
|
||||||
|
ProcessExecutorResult returnCode = null;
|
||||||
|
try {
|
||||||
|
returnCode =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||||
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
|
if (returnCode.getRc() == 0) {
|
||||||
|
// Update current file to the Ghostscript output
|
||||||
|
Files.copy(gsOutputFile, currentFile, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
|
||||||
|
long postGsSize = Files.size(currentFile);
|
||||||
|
double gsReduction = 100.0 - ((postGsSize * 100.0) / preGsSize);
|
||||||
|
log.info(
|
||||||
|
"Post-Ghostscript file size: {} (reduced by {}%)",
|
||||||
|
GeneralUtils.formatBytes(postGsSize), String.format("%.1f", gsReduction));
|
||||||
|
} else {
|
||||||
|
log.warn("Ghostscript compression failed with return code: {}", returnCode.getRc());
|
||||||
|
throw new IOException("Ghostscript compression failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Ghostscript compression failed, will fallback to other methods", e);
|
||||||
|
throw new IOException("Ghostscript compression failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Run QPDF compression
|
// Run QPDF compression
|
||||||
private void applyQpdfCompression(
|
private void applyQpdfCompression(
|
||||||
OptimizePdfRequest request, int optimizeLevel, Path currentFile, List<Path> tempFiles)
|
OptimizePdfRequest request, int optimizeLevel, Path currentFile, List<Path> tempFiles)
|
||||||
|
|||||||
+2
-1
@@ -26,6 +26,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
|
|
||||||
import stirling.software.common.model.api.PDFFile;
|
import stirling.software.common.model.api.PDFFile;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -134,7 +135,7 @@ public class DecompressPdfController {
|
|||||||
stream.setInt(COSName.LENGTH, decompressedBytes.length);
|
stream.setInt(COSName.LENGTH, decompressedBytes.length);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Error decompressing stream", e);
|
ExceptionUtils.logException("stream decompression", e);
|
||||||
// Continue processing other streams even if this one fails
|
// Continue processing other streams even if this one fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -33,6 +33,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
|
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.CheckProgramInstall;
|
import stirling.software.common.util.CheckProgramInstall;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.ProcessExecutor;
|
import stirling.software.common.util.ProcessExecutor;
|
||||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
@@ -72,7 +73,8 @@ public class ExtractImageScansController {
|
|||||||
List<Path> tempDirs = new ArrayList<>();
|
List<Path> tempDirs = new ArrayList<>();
|
||||||
|
|
||||||
if (!CheckProgramInstall.isPythonAvailable()) {
|
if (!CheckProgramInstall.isPythonAvailable()) {
|
||||||
throw new IOException("Python is not installed.");
|
throw ExceptionUtils.createIOException(
|
||||||
|
"error.toolNotInstalled", "{0} is not installed", null, "Python");
|
||||||
}
|
}
|
||||||
|
|
||||||
String pythonVersion = CheckProgramInstall.getAvailablePythonCommand();
|
String pythonVersion = CheckProgramInstall.getAvailablePythonCommand();
|
||||||
|
|||||||
+79
-55
@@ -41,6 +41,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
|
|
||||||
import stirling.software.SPDF.model.api.PDFExtractImagesRequest;
|
import stirling.software.SPDF.model.api.PDFExtractImagesRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.ImageProcessingUtils;
|
import stirling.software.common.util.ImageProcessingUtils;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@@ -90,39 +91,57 @@ public class ExtractImagesController {
|
|||||||
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
|
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
|
||||||
Set<Future<Void>> futures = new HashSet<>();
|
Set<Future<Void>> futures = new HashSet<>();
|
||||||
|
|
||||||
// Iterate over each page
|
// Safely iterate over each page, handling corrupt PDFs where page count might be wrong
|
||||||
for (int pgNum = 0; pgNum < document.getPages().getCount(); pgNum++) {
|
try {
|
||||||
PDPage page = document.getPage(pgNum);
|
int pageCount = document.getPages().getCount();
|
||||||
Future<Void> future =
|
log.debug("Document reports {} pages", pageCount);
|
||||||
executor.submit(
|
|
||||||
() -> {
|
|
||||||
// Use the page number directly from the iterator, so no need to
|
|
||||||
// calculate manually
|
|
||||||
int pageNum = document.getPages().indexOf(page) + 1;
|
|
||||||
|
|
||||||
try {
|
int consecutiveFailures = 0;
|
||||||
// Call the image extraction method for each page
|
|
||||||
extractImagesFromPage(
|
|
||||||
page,
|
|
||||||
format,
|
|
||||||
filename,
|
|
||||||
pageNum,
|
|
||||||
processedImages,
|
|
||||||
zos,
|
|
||||||
allowDuplicates);
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Log the error and continue processing other pages
|
|
||||||
log.error(
|
|
||||||
"Error extracting images from page {}: {}",
|
|
||||||
pageNum,
|
|
||||||
e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return null; // Callable requires a return type
|
for (int pgNum = 0; pgNum < pageCount; pgNum++) {
|
||||||
});
|
try {
|
||||||
|
PDPage page = document.getPage(pgNum);
|
||||||
|
consecutiveFailures = 0; // Reset on success
|
||||||
|
final int currentPageNum = pgNum + 1; // Convert to 1-based page numbering
|
||||||
|
Future<Void> future =
|
||||||
|
executor.submit(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
// Call the image extraction method for each page
|
||||||
|
extractImagesFromPage(
|
||||||
|
page,
|
||||||
|
format,
|
||||||
|
filename,
|
||||||
|
currentPageNum,
|
||||||
|
processedImages,
|
||||||
|
zos,
|
||||||
|
allowDuplicates);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Log the error and continue processing other pages
|
||||||
|
ExceptionUtils.logException(
|
||||||
|
"image extraction from page "
|
||||||
|
+ currentPageNum,
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
|
||||||
// Add the Future object to the list to track completion
|
return null; // Callable requires a return type
|
||||||
futures.add(future);
|
});
|
||||||
|
|
||||||
|
// Add the Future object to the list to track completion
|
||||||
|
futures.add(future);
|
||||||
|
} catch (Exception e) {
|
||||||
|
consecutiveFailures++;
|
||||||
|
ExceptionUtils.logException("page access for page " + (pgNum + 1), e);
|
||||||
|
|
||||||
|
if (consecutiveFailures >= 3) {
|
||||||
|
log.warn("Stopping page iteration after 3 consecutive failures");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
ExceptionUtils.logException("page count determination", e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all tasks to complete
|
// Wait for all tasks to complete
|
||||||
@@ -180,34 +199,39 @@ public class ExtractImagesController {
|
|||||||
}
|
}
|
||||||
int count = 1;
|
int count = 1;
|
||||||
for (COSName name : page.getResources().getXObjectNames()) {
|
for (COSName name : page.getResources().getXObjectNames()) {
|
||||||
if (page.getResources().isImageXObject(name)) {
|
try {
|
||||||
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
|
if (page.getResources().isImageXObject(name)) {
|
||||||
if (!allowDuplicates) {
|
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
|
||||||
byte[] data = ImageProcessingUtils.getImageData(image.getImage());
|
if (!allowDuplicates) {
|
||||||
byte[] imageHash = md.digest(data);
|
byte[] data = ImageProcessingUtils.getImageData(image.getImage());
|
||||||
synchronized (processedImages) {
|
byte[] imageHash = md.digest(data);
|
||||||
if (processedImages.stream()
|
synchronized (processedImages) {
|
||||||
.anyMatch(hash -> Arrays.equals(hash, imageHash))) {
|
if (processedImages.stream()
|
||||||
continue; // Skip already processed images
|
.anyMatch(hash -> Arrays.equals(hash, imageHash))) {
|
||||||
|
continue; // Skip already processed images
|
||||||
|
}
|
||||||
|
processedImages.add(imageHash);
|
||||||
}
|
}
|
||||||
processedImages.add(imageHash);
|
}
|
||||||
|
|
||||||
|
RenderedImage renderedImage = image.getImage();
|
||||||
|
|
||||||
|
// Convert to standard RGB colorspace if needed
|
||||||
|
BufferedImage bufferedImage = convertToRGB(renderedImage, format);
|
||||||
|
|
||||||
|
// Write image to zip file
|
||||||
|
String imageName = filename + "_page_" + pageNum + "_" + count++ + "." + format;
|
||||||
|
synchronized (zos) {
|
||||||
|
zos.putNextEntry(new ZipEntry(imageName));
|
||||||
|
ByteArrayOutputStream imageBaos = new ByteArrayOutputStream();
|
||||||
|
ImageIO.write(bufferedImage, format, imageBaos);
|
||||||
|
zos.write(imageBaos.toByteArray());
|
||||||
|
zos.closeEntry();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
RenderedImage renderedImage = image.getImage();
|
ExceptionUtils.logException("image extraction", e);
|
||||||
|
throw ExceptionUtils.handlePdfException(e, "during image extraction");
|
||||||
// Convert to standard RGB colorspace if needed
|
|
||||||
BufferedImage bufferedImage = convertToRGB(renderedImage, format);
|
|
||||||
|
|
||||||
// Write image to zip file
|
|
||||||
String imageName = filename + "_page_" + pageNum + "_" + count++ + "." + format;
|
|
||||||
synchronized (zos) {
|
|
||||||
zos.putNextEntry(new ZipEntry(imageName));
|
|
||||||
ByteArrayOutputStream imageBaos = new ByteArrayOutputStream();
|
|
||||||
ImageIO.write(bufferedImage, format, imageBaos);
|
|
||||||
zos.write(imageBaos.toByteArray());
|
|
||||||
zos.closeEntry();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-2
@@ -47,6 +47,11 @@ public class FakeScanController {
|
|||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
private static final Random RANDOM = new Random();
|
private static final Random RANDOM = new Random();
|
||||||
|
|
||||||
|
// Size limits to prevent OutOfMemoryError
|
||||||
|
private static final int MAX_IMAGE_WIDTH = 8192;
|
||||||
|
private static final int MAX_IMAGE_HEIGHT = 8192;
|
||||||
|
private static final long MAX_IMAGE_PIXELS = 16_777_216; // 4096x4096
|
||||||
|
|
||||||
@PostMapping(value = "/fake-scan", consumes = "multipart/form-data")
|
@PostMapping(value = "/fake-scan", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert PDF to look like a scanned document",
|
summary = "Convert PDF to look like a scanned document",
|
||||||
@@ -82,8 +87,46 @@ public class FakeScanController {
|
|||||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
|
|
||||||
for (int i = 0; i < document.getNumberOfPages(); i++) {
|
for (int i = 0; i < document.getNumberOfPages(); i++) {
|
||||||
// Render page to image with specified resolution
|
// Get page dimensions to calculate safe resolution
|
||||||
BufferedImage image = pdfRenderer.renderImageWithDPI(i, resolution);
|
PDRectangle pageSize = document.getPage(i).getMediaBox();
|
||||||
|
float pageWidthPts = pageSize.getWidth();
|
||||||
|
float pageHeightPts = pageSize.getHeight();
|
||||||
|
|
||||||
|
// Calculate what the image dimensions would be at the requested resolution
|
||||||
|
int projectedWidth = (int) Math.ceil(pageWidthPts * resolution / 72.0);
|
||||||
|
int projectedHeight = (int) Math.ceil(pageHeightPts * resolution / 72.0);
|
||||||
|
long projectedPixels = (long) projectedWidth * projectedHeight;
|
||||||
|
|
||||||
|
// Calculate safe resolution that stays within limits
|
||||||
|
int safeResolution = resolution;
|
||||||
|
if (projectedWidth > MAX_IMAGE_WIDTH
|
||||||
|
|| projectedHeight > MAX_IMAGE_HEIGHT
|
||||||
|
|| projectedPixels > MAX_IMAGE_PIXELS) {
|
||||||
|
double widthScale = (double) MAX_IMAGE_WIDTH / projectedWidth;
|
||||||
|
double heightScale = (double) MAX_IMAGE_HEIGHT / projectedHeight;
|
||||||
|
double pixelScale = Math.sqrt((double) MAX_IMAGE_PIXELS / projectedPixels);
|
||||||
|
double minScale = Math.min(Math.min(widthScale, heightScale), pixelScale);
|
||||||
|
safeResolution = (int) Math.max(72, resolution * minScale);
|
||||||
|
|
||||||
|
log.warn(
|
||||||
|
"Page {} would be too large at {}dpi ({}x{} pixels). Reducing to {}dpi",
|
||||||
|
i + 1,
|
||||||
|
resolution,
|
||||||
|
projectedWidth,
|
||||||
|
projectedHeight,
|
||||||
|
safeResolution);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render page to image with safe resolution
|
||||||
|
BufferedImage image = pdfRenderer.renderImageWithDPI(i, safeResolution);
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
"Processing page {} with dimensions {}x{} ({} pixels) at {}dpi",
|
||||||
|
i + 1,
|
||||||
|
image.getWidth(),
|
||||||
|
image.getHeight(),
|
||||||
|
(long) image.getWidth() * image.getHeight(),
|
||||||
|
safeResolution);
|
||||||
|
|
||||||
// 1. Convert to grayscale or keep color
|
// 1. Convert to grayscale or keep color
|
||||||
BufferedImage processed;
|
BufferedImage processed;
|
||||||
|
|||||||
+273
-76
@@ -2,6 +2,7 @@ package stirling.software.SPDF.controller.api.misc;
|
|||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
@@ -29,12 +30,17 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||||
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.ProcessExecutor;
|
import stirling.software.common.util.ProcessExecutor;
|
||||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
import stirling.software.common.util.TempDirectory;
|
||||||
|
import stirling.software.common.util.TempFile;
|
||||||
import stirling.software.common.util.TempFileManager;
|
import stirling.software.common.util.TempFileManager;
|
||||||
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/misc")
|
@RequestMapping("/api/v1/misc")
|
||||||
@@ -46,6 +52,15 @@ public class OCRController {
|
|||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
private final TempFileManager tempFileManager;
|
private final TempFileManager tempFileManager;
|
||||||
|
private final EndpointConfiguration endpointConfiguration;
|
||||||
|
|
||||||
|
private boolean isOcrMyPdfEnabled() {
|
||||||
|
return endpointConfiguration.isGroupEnabled("OCRmyPDF");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTesseractEnabled() {
|
||||||
|
return endpointConfiguration.isGroupEnabled("tesseract");
|
||||||
|
}
|
||||||
|
|
||||||
/** Gets the list of available Tesseract languages from the tessdata directory */
|
/** Gets the list of available Tesseract languages from the tessdata directory */
|
||||||
public List<String> getAvailableTesseractLanguages() {
|
public List<String> getAvailableTesseractLanguages() {
|
||||||
@@ -63,39 +78,261 @@ public class OCRController {
|
|||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Process PDF files with OCR using Tesseract",
|
summary = "Process a PDF file with OCR",
|
||||||
description =
|
description =
|
||||||
"Takes a PDF file as input, performs OCR using specified languages and OCR type"
|
"This endpoint processes a PDF file using OCR (Optical Character Recognition). "
|
||||||
+ " (skip-text/force-ocr), and returns the processed PDF. Input:PDF"
|
+ "Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType, and removeImagesAfter options. "
|
||||||
+ " Output:PDF Type:SISO")
|
+ "Uses OCRmyPDF if available, falls back to Tesseract. Input:PDF Output:PDF Type:SI-Conditional")
|
||||||
public ResponseEntity<byte[]> processPdfWithOCR(
|
public ResponseEntity<byte[]> processPdfWithOCR(
|
||||||
@ModelAttribute ProcessPdfWithOcrRequest request)
|
@ModelAttribute ProcessPdfWithOcrRequest request)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
List<String> languages = request.getLanguages();
|
List<String> selectedLanguages = request.getLanguages();
|
||||||
|
Boolean sidecar = request.isSidecar();
|
||||||
|
Boolean deskew = request.isDeskew();
|
||||||
|
Boolean clean = request.isClean();
|
||||||
|
Boolean cleanFinal = request.isCleanFinal();
|
||||||
String ocrType = request.getOcrType();
|
String ocrType = request.getOcrType();
|
||||||
|
String ocrRenderType = request.getOcrRenderType();
|
||||||
|
Boolean removeImagesAfter = request.isRemoveImagesAfter();
|
||||||
|
|
||||||
// Create a temp directory using TempFileManager directly
|
if (selectedLanguages == null || selectedLanguages.isEmpty()) {
|
||||||
Path tempDirPath = tempFileManager.createTempDirectory();
|
throw ExceptionUtils.createOcrLanguageRequiredException();
|
||||||
File tempDir = tempDirPath.toFile();
|
}
|
||||||
|
|
||||||
try {
|
if (!"hocr".equals(ocrRenderType) && !"sandwich".equals(ocrRenderType)) {
|
||||||
File tempInputFile = new File(tempDir, "input.pdf");
|
throw new IOException("ocrRenderType wrong");
|
||||||
File tempOutputDir = new File(tempDir, "output");
|
}
|
||||||
File tempImagesDir = new File(tempDir, "images");
|
|
||||||
File finalOutputFile = new File(tempDir, "final_output.pdf");
|
// Get available Tesseract languages
|
||||||
|
List<String> availableLanguages = getAvailableTesseractLanguages();
|
||||||
|
|
||||||
|
// Validate selected languages
|
||||||
|
selectedLanguages =
|
||||||
|
selectedLanguages.stream().filter(availableLanguages::contains).toList();
|
||||||
|
|
||||||
|
if (selectedLanguages.isEmpty()) {
|
||||||
|
throw ExceptionUtils.createOcrInvalidLanguagesException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use try-with-resources for proper temp file management
|
||||||
|
try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf");
|
||||||
|
TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf")) {
|
||||||
|
|
||||||
|
inputFile.transferTo(tempInputFile.getFile());
|
||||||
|
|
||||||
|
TempFile sidecarTextFile = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use OCRmyPDF if available (no fallback - error if it fails)
|
||||||
|
if (isOcrMyPdfEnabled()) {
|
||||||
|
if (sidecar != null && sidecar) {
|
||||||
|
sidecarTextFile = new TempFile(tempFileManager, ".txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
processWithOcrMyPdf(
|
||||||
|
selectedLanguages,
|
||||||
|
sidecar,
|
||||||
|
deskew,
|
||||||
|
clean,
|
||||||
|
cleanFinal,
|
||||||
|
ocrType,
|
||||||
|
ocrRenderType,
|
||||||
|
removeImagesAfter,
|
||||||
|
tempInputFile.getPath(),
|
||||||
|
tempOutputFile.getPath(),
|
||||||
|
sidecarTextFile != null ? sidecarTextFile.getPath() : null);
|
||||||
|
log.info("OCRmyPDF processing completed successfully");
|
||||||
|
}
|
||||||
|
// Use Tesseract only if OCRmyPDF is not available
|
||||||
|
else if (isTesseractEnabled()) {
|
||||||
|
processWithTesseract(
|
||||||
|
selectedLanguages,
|
||||||
|
ocrType,
|
||||||
|
tempInputFile.getPath(),
|
||||||
|
tempOutputFile.getPath());
|
||||||
|
log.info("Tesseract processing completed successfully");
|
||||||
|
} else {
|
||||||
|
throw ExceptionUtils.createOcrToolsUnavailableException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the processed PDF file
|
||||||
|
byte[] pdfBytes = Files.readAllBytes(tempOutputFile.getPath());
|
||||||
|
|
||||||
|
// Return the OCR processed PDF as a response
|
||||||
|
String outputFilename =
|
||||||
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_OCR.pdf";
|
||||||
|
|
||||||
|
if (sidecar != null && sidecar && sidecarTextFile != null) {
|
||||||
|
// Create a zip file containing both the PDF and the text file
|
||||||
|
String outputZipFilename =
|
||||||
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_OCR.zip";
|
||||||
|
|
||||||
|
try (TempFile tempZipFile = new TempFile(tempFileManager, ".zip");
|
||||||
|
ZipOutputStream zipOut =
|
||||||
|
new ZipOutputStream(
|
||||||
|
Files.newOutputStream(tempZipFile.getPath()))) {
|
||||||
|
|
||||||
|
// Add PDF file to the zip
|
||||||
|
ZipEntry pdfEntry = new ZipEntry(outputFilename);
|
||||||
|
zipOut.putNextEntry(pdfEntry);
|
||||||
|
zipOut.write(pdfBytes);
|
||||||
|
zipOut.closeEntry();
|
||||||
|
|
||||||
|
// Add text file to the zip
|
||||||
|
ZipEntry txtEntry = new ZipEntry(outputFilename.replace(".pdf", ".txt"));
|
||||||
|
zipOut.putNextEntry(txtEntry);
|
||||||
|
Files.copy(sidecarTextFile.getPath(), zipOut);
|
||||||
|
zipOut.closeEntry();
|
||||||
|
|
||||||
|
zipOut.finish();
|
||||||
|
|
||||||
|
byte[] zipBytes = Files.readAllBytes(tempZipFile.getPath());
|
||||||
|
|
||||||
|
// Return the zip file containing both the PDF and the text file
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Return the OCR processed PDF as a response
|
||||||
|
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// Clean up sidecar temp file if created
|
||||||
|
if (sidecarTextFile != null) {
|
||||||
|
try {
|
||||||
|
sidecarTextFile.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to close sidecar temp file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processWithOcrMyPdf(
|
||||||
|
List<String> selectedLanguages,
|
||||||
|
Boolean sidecar,
|
||||||
|
Boolean deskew,
|
||||||
|
Boolean clean,
|
||||||
|
Boolean cleanFinal,
|
||||||
|
String ocrType,
|
||||||
|
String ocrRenderType,
|
||||||
|
Boolean removeImagesAfter,
|
||||||
|
Path tempInputFile,
|
||||||
|
Path tempOutputFile,
|
||||||
|
Path sidecarTextPath)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
|
||||||
|
// Build OCRmyPDF command
|
||||||
|
String languageOption = String.join("+", selectedLanguages);
|
||||||
|
|
||||||
|
List<String> command =
|
||||||
|
new ArrayList<>(
|
||||||
|
Arrays.asList(
|
||||||
|
"ocrmypdf",
|
||||||
|
"--verbose",
|
||||||
|
"2",
|
||||||
|
"--output-type",
|
||||||
|
"pdf",
|
||||||
|
"--pdf-renderer",
|
||||||
|
ocrRenderType));
|
||||||
|
|
||||||
|
if (sidecar != null && sidecar && sidecarTextPath != null) {
|
||||||
|
command.add("--sidecar");
|
||||||
|
command.add(sidecarTextPath.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deskew != null && deskew) {
|
||||||
|
command.add("--deskew");
|
||||||
|
}
|
||||||
|
if (clean != null && clean) {
|
||||||
|
command.add("--clean");
|
||||||
|
}
|
||||||
|
if (cleanFinal != null && cleanFinal) {
|
||||||
|
command.add("--clean-final");
|
||||||
|
}
|
||||||
|
if (ocrType != null && !"".equals(ocrType)) {
|
||||||
|
if ("skip-text".equals(ocrType)) {
|
||||||
|
command.add("--skip-text");
|
||||||
|
} else if ("force-ocr".equals(ocrType)) {
|
||||||
|
command.add("--force-ocr");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
command.addAll(
|
||||||
|
Arrays.asList(
|
||||||
|
"--language",
|
||||||
|
languageOption,
|
||||||
|
tempInputFile.toString(),
|
||||||
|
tempOutputFile.toString()));
|
||||||
|
|
||||||
|
// Run CLI command
|
||||||
|
ProcessExecutorResult result =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
|
||||||
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
|
if (result.getRc() != 0
|
||||||
|
&& result.getMessages().contains("multiprocessing/synchronize.py")
|
||||||
|
&& result.getMessages().contains("OSError: [Errno 38] Function not implemented")) {
|
||||||
|
command.add("--jobs");
|
||||||
|
command.add("1");
|
||||||
|
result =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
|
||||||
|
.runCommandWithOutputHandling(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.getRc() != 0) {
|
||||||
|
throw new IOException("OCRmyPDF failed with return code: " + result.getRc());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove images from the OCR processed PDF if the flag is set to true
|
||||||
|
if (removeImagesAfter != null && removeImagesAfter) {
|
||||||
|
try (TempFile tempPdfWithoutImages = new TempFile(tempFileManager, "_no_images.pdf")) {
|
||||||
|
List<String> gsCommand =
|
||||||
|
Arrays.asList(
|
||||||
|
"gs",
|
||||||
|
"-sDEVICE=pdfwrite",
|
||||||
|
"-dFILTERIMAGE",
|
||||||
|
"-o",
|
||||||
|
tempPdfWithoutImages.getPath().toString(),
|
||||||
|
tempOutputFile.toString());
|
||||||
|
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||||
|
.runCommandWithOutputHandling(gsCommand);
|
||||||
|
|
||||||
|
// Replace output file with version without images
|
||||||
|
Files.copy(
|
||||||
|
tempPdfWithoutImages.getPath(),
|
||||||
|
tempOutputFile,
|
||||||
|
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processWithTesseract(
|
||||||
|
List<String> selectedLanguages, String ocrType, Path tempInputFile, Path tempOutputFile)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
|
||||||
|
// Create temp directory for Tesseract processing
|
||||||
|
try (TempDirectory tempDir = new TempDirectory(tempFileManager)) {
|
||||||
|
File tempOutputDir = new File(tempDir.getPath().toFile(), "output");
|
||||||
|
File tempImagesDir = new File(tempDir.getPath().toFile(), "images");
|
||||||
|
File finalOutputFile = new File(tempDir.getPath().toFile(), "final_output.pdf");
|
||||||
|
|
||||||
// Create directories
|
// Create directories
|
||||||
tempOutputDir.mkdirs();
|
tempOutputDir.mkdirs();
|
||||||
tempImagesDir.mkdirs();
|
tempImagesDir.mkdirs();
|
||||||
|
|
||||||
// Save input file
|
|
||||||
inputFile.transferTo(tempInputFile);
|
|
||||||
|
|
||||||
PDFMergerUtility merger = new PDFMergerUtility();
|
PDFMergerUtility merger = new PDFMergerUtility();
|
||||||
merger.setDestinationFileName(finalOutputFile.toString());
|
merger.setDestinationFileName(finalOutputFile.toString());
|
||||||
|
|
||||||
try (PDDocument document = pdfDocumentFactory.load(tempInputFile)) {
|
try (PDDocument document = pdfDocumentFactory.load(tempInputFile.toFile())) {
|
||||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
int pageCount = document.getNumberOfPages();
|
int pageCount = document.getNumberOfPages();
|
||||||
|
|
||||||
@@ -135,35 +372,24 @@ public class OCRController {
|
|||||||
new File(tempOutputDir, String.format("page_%d", pageNum))
|
new File(tempOutputDir, String.format("page_%d", pageNum))
|
||||||
.toString());
|
.toString());
|
||||||
command.add("-l");
|
command.add("-l");
|
||||||
command.add(String.join("+", languages));
|
command.add(String.join("+", selectedLanguages));
|
||||||
// Always output PDF
|
command.add("pdf"); // Always output PDF
|
||||||
command.add("pdf");
|
|
||||||
|
|
||||||
// Use ProcessExecutor to run tesseract command
|
ProcessExecutorResult result =
|
||||||
try {
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.TESSERACT)
|
||||||
ProcessExecutorResult result =
|
.runCommandWithOutputHandling(command);
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.TESSERACT)
|
|
||||||
.runCommandWithOutputHandling(command);
|
|
||||||
|
|
||||||
log.debug(
|
if (result.getRc() != 0) {
|
||||||
"Tesseract OCR completed for page {} with exit code {}",
|
throw ExceptionUtils.createRuntimeException(
|
||||||
pageNum,
|
"error.commandFailed",
|
||||||
|
"{0} command failed with exit code: {1}",
|
||||||
|
null,
|
||||||
|
"Tesseract",
|
||||||
result.getRc());
|
result.getRc());
|
||||||
|
|
||||||
// Add OCR'd PDF to merger
|
|
||||||
merger.addSource(pageOutputPath);
|
|
||||||
} catch (IOException | InterruptedException e) {
|
|
||||||
log.error(
|
|
||||||
"Error processing page {} with tesseract: {}",
|
|
||||||
pageNum,
|
|
||||||
e.getMessage());
|
|
||||||
// If OCR fails, fall back to the original page
|
|
||||||
try (PDDocument pageDoc = new PDDocument()) {
|
|
||||||
pageDoc.addPage(page);
|
|
||||||
pageDoc.save(pageOutputPath);
|
|
||||||
merger.addSource(pageOutputPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add OCR'd PDF to merger
|
||||||
|
merger.addSource(pageOutputPath);
|
||||||
} else {
|
} else {
|
||||||
// Save original page without OCR
|
// Save original page without OCR
|
||||||
try (PDDocument pageDoc = new PDDocument()) {
|
try (PDDocument pageDoc = new PDDocument()) {
|
||||||
@@ -178,40 +404,11 @@ public class OCRController {
|
|||||||
// Merge all pages into final PDF
|
// Merge all pages into final PDF
|
||||||
merger.mergeDocuments(null);
|
merger.mergeDocuments(null);
|
||||||
|
|
||||||
// Read the final PDF file
|
// Copy final output to the expected location
|
||||||
byte[] pdfContent = java.nio.file.Files.readAllBytes(finalOutputFile.toPath());
|
Files.copy(
|
||||||
String outputFilename =
|
finalOutputFile.toPath(),
|
||||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
tempOutputFile,
|
||||||
.replaceFirst("[.][^.]+$", "")
|
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||||
+ "_OCR.pdf";
|
|
||||||
|
|
||||||
return ResponseEntity.ok()
|
|
||||||
.header(
|
|
||||||
"Content-Disposition",
|
|
||||||
"attachment; filename=\"" + outputFilename + "\"")
|
|
||||||
.contentType(MediaType.APPLICATION_PDF)
|
|
||||||
.body(pdfContent);
|
|
||||||
} finally {
|
|
||||||
// Clean up the temp directory and all its contents
|
|
||||||
tempFileManager.deleteTempDirectory(tempDirPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addFileToZip(File file, String filename, ZipOutputStream zipOut)
|
|
||||||
throws IOException {
|
|
||||||
if (!file.exists()) {
|
|
||||||
log.warn("File {} does not exist, skipping", file);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try (FileInputStream fis = new FileInputStream(file)) {
|
|
||||||
ZipEntry zipEntry = new ZipEntry(filename);
|
|
||||||
zipOut.putNextEntry(zipEntry);
|
|
||||||
byte[] buffer = new byte[1024];
|
|
||||||
int length;
|
|
||||||
while ((length = fis.read(buffer)) >= 0) {
|
|
||||||
zipOut.write(buffer, 0, length);
|
|
||||||
}
|
|
||||||
zipOut.closeEntry();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+73
-15
@@ -16,7 +16,9 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||||
import stirling.software.common.model.api.PDFFile;
|
import stirling.software.common.model.api.PDFFile;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.ProcessExecutor;
|
import stirling.software.common.util.ProcessExecutor;
|
||||||
@@ -28,17 +30,27 @@ import stirling.software.common.util.WebResponseUtils;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/misc")
|
@RequestMapping("/api/v1/misc")
|
||||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class RepairController {
|
public class RepairController {
|
||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
private final TempFileManager tempFileManager;
|
private final TempFileManager tempFileManager;
|
||||||
|
private final EndpointConfiguration endpointConfiguration;
|
||||||
|
|
||||||
|
private boolean isGhostscriptEnabled() {
|
||||||
|
return endpointConfiguration.isGroupEnabled("Ghostscript");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isQpdfEnabled() {
|
||||||
|
return endpointConfiguration.isGroupEnabled("qpdf");
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/repair")
|
@PostMapping(consumes = "multipart/form-data", value = "/repair")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Repair a PDF file",
|
summary = "Repair a PDF file",
|
||||||
description =
|
description =
|
||||||
"This endpoint repairs a given PDF file by running qpdf command. The PDF is"
|
"This endpoint repairs a given PDF file by running Ghostscript (primary), qpdf (fallback), or PDFBox (if no external tools available). The PDF is"
|
||||||
+ " first saved to a temporary location, repaired, read back, and then"
|
+ " first saved to a temporary location, repaired, read back, and then"
|
||||||
+ " returned as a response. Input:PDF Output:PDF Type:SISO")
|
+ " returned as a response. Input:PDF Output:PDF Type:SISO")
|
||||||
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile file)
|
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile file)
|
||||||
@@ -46,25 +58,71 @@ public class RepairController {
|
|||||||
MultipartFile inputFile = file.getFileInput();
|
MultipartFile inputFile = file.getFileInput();
|
||||||
|
|
||||||
// Use TempFile with try-with-resources for automatic cleanup
|
// Use TempFile with try-with-resources for automatic cleanup
|
||||||
try (TempFile tempFile = new TempFile(tempFileManager, ".pdf")) {
|
try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf");
|
||||||
|
TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf")) {
|
||||||
|
|
||||||
// Save the uploaded file to the temporary location
|
// Save the uploaded file to the temporary location
|
||||||
inputFile.transferTo(tempFile.getFile());
|
inputFile.transferTo(tempInputFile.getFile());
|
||||||
|
|
||||||
List<String> command = new ArrayList<>();
|
boolean repairSuccess = false;
|
||||||
command.add("qpdf");
|
|
||||||
command.add("--replace-input"); // Automatically fixes problems it can
|
|
||||||
command.add("--qdf"); // Linearizes and normalizes PDF structure
|
|
||||||
command.add("--object-streams=disable"); // Can help with some corruptions
|
|
||||||
command.add(tempFile.getFile().getAbsolutePath());
|
|
||||||
|
|
||||||
ProcessExecutorResult returnCode =
|
// Try Ghostscript first if available
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF)
|
if (isGhostscriptEnabled()) {
|
||||||
.runCommandWithOutputHandling(command);
|
try {
|
||||||
|
List<String> gsCommand = new ArrayList<>();
|
||||||
|
gsCommand.add("gs");
|
||||||
|
gsCommand.add("-o");
|
||||||
|
gsCommand.add(tempOutputFile.getPath().toString());
|
||||||
|
gsCommand.add("-sDEVICE=pdfwrite");
|
||||||
|
gsCommand.add(tempInputFile.getPath().toString());
|
||||||
|
|
||||||
// Read the optimized PDF file
|
ProcessExecutorResult gsResult =
|
||||||
byte[] pdfBytes = pdfDocumentFactory.loadToBytes(tempFile.getFile());
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||||
|
.runCommandWithOutputHandling(gsCommand);
|
||||||
|
|
||||||
// Return the optimized PDF as a response
|
if (gsResult.getRc() == 0) {
|
||||||
|
repairSuccess = true;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Log and continue to QPDF fallback
|
||||||
|
log.warn("Ghostscript repair failed, trying QPDF fallback: ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to QPDF if Ghostscript failed or not available
|
||||||
|
if (!repairSuccess && isQpdfEnabled()) {
|
||||||
|
List<String> qpdfCommand = new ArrayList<>();
|
||||||
|
qpdfCommand.add("qpdf");
|
||||||
|
qpdfCommand.add("--replace-input"); // Automatically fixes problems it can
|
||||||
|
qpdfCommand.add("--qdf"); // Linearizes and normalizes PDF structure
|
||||||
|
qpdfCommand.add("--object-streams=disable"); // Can help with some corruptions
|
||||||
|
qpdfCommand.add(tempInputFile.getPath().toString());
|
||||||
|
qpdfCommand.add(tempOutputFile.getPath().toString());
|
||||||
|
|
||||||
|
ProcessExecutorResult qpdfResult =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF)
|
||||||
|
.runCommandWithOutputHandling(qpdfCommand);
|
||||||
|
|
||||||
|
repairSuccess = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use PDFBox as last resort if no external tools are available
|
||||||
|
if (!repairSuccess) {
|
||||||
|
if (!isGhostscriptEnabled() && !isQpdfEnabled()) {
|
||||||
|
// Basic PDFBox repair - load and save to fix structural issues
|
||||||
|
try (var document = pdfDocumentFactory.load(tempInputFile.getFile())) {
|
||||||
|
document.save(tempOutputFile.getFile());
|
||||||
|
repairSuccess = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IOException("PDF repair failed with available tools");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the repaired PDF file
|
||||||
|
byte[] pdfBytes = pdfDocumentFactory.loadToBytes(tempOutputFile.getFile());
|
||||||
|
|
||||||
|
// Return the repaired PDF as a response
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
.replaceFirst("[.][^.]+$", "")
|
.replaceFirst("[.][^.]+$", "")
|
||||||
|
|||||||
+34
-2
@@ -150,11 +150,22 @@ public class PipelineProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!hasInputFileType) {
|
if (!hasInputFileType) {
|
||||||
|
String filename = file.getFilename();
|
||||||
|
String providedExtension = "no extension";
|
||||||
|
if (filename != null && filename.contains(".")) {
|
||||||
|
providedExtension =
|
||||||
|
filename.substring(filename.lastIndexOf(".")).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
logPrintStream.println(
|
logPrintStream.println(
|
||||||
"No files with extension "
|
"No files with extension "
|
||||||
+ String.join(", ", inputFileTypes)
|
+ String.join(", ", inputFileTypes)
|
||||||
+ " found for operation "
|
+ " found for operation "
|
||||||
+ operation);
|
+ operation
|
||||||
|
+ ". Provided file '"
|
||||||
|
+ filename
|
||||||
|
+ "' has extension: "
|
||||||
|
+ providedExtension);
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,11 +214,32 @@ public class PipelineProcessor {
|
|||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Get details about what files were actually provided
|
||||||
|
List<String> providedExtensions =
|
||||||
|
outputFiles.stream()
|
||||||
|
.map(
|
||||||
|
file -> {
|
||||||
|
String filename = file.getFilename();
|
||||||
|
if (filename != null && filename.contains(".")) {
|
||||||
|
return filename.substring(
|
||||||
|
filename.lastIndexOf("."))
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
return "no extension";
|
||||||
|
})
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
|
||||||
logPrintStream.println(
|
logPrintStream.println(
|
||||||
"No files with extension "
|
"No files with extension "
|
||||||
+ String.join(", ", inputFileTypes)
|
+ String.join(", ", inputFileTypes)
|
||||||
+ " found for multi-input operation "
|
+ " found for multi-input operation "
|
||||||
+ operation);
|
+ operation
|
||||||
|
+ ". Provided files have extensions: "
|
||||||
|
+ String.join(", ", providedExtensions)
|
||||||
|
+ " (total files: "
|
||||||
|
+ outputFiles.size()
|
||||||
|
+ ")");
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-4
@@ -65,6 +65,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.micrometer.common.util.StringUtils;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
@@ -73,6 +74,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
|
|
||||||
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
|
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -132,7 +134,7 @@ public class CertSignController {
|
|||||||
}
|
}
|
||||||
doc.saveIncremental(output);
|
doc.saveIncremental(output);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("exception", e);
|
ExceptionUtils.logException("PDF signing", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,8 +167,11 @@ public class CertSignController {
|
|||||||
Integer pageNumber = request.getPageNumber() != null ? (request.getPageNumber() - 1) : null;
|
Integer pageNumber = request.getPageNumber() != null ? (request.getPageNumber() - 1) : null;
|
||||||
Boolean showLogo = request.getShowLogo();
|
Boolean showLogo = request.getShowLogo();
|
||||||
|
|
||||||
if (certType == null) {
|
if (StringUtils.isBlank(certType)) {
|
||||||
throw new IllegalArgumentException("Cert type must be provided");
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.optionsNotSpecified",
|
||||||
|
"{0} options are not specified",
|
||||||
|
"certificate type");
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyStore ks = null;
|
KeyStore ks = null;
|
||||||
@@ -189,7 +194,10 @@ public class CertSignController {
|
|||||||
ks.load(jksfile.getInputStream(), password.toCharArray());
|
ks.load(jksfile.getInputStream(), password.toCharArray());
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Invalid cert type: " + certType);
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.invalidArgument",
|
||||||
|
"Invalid argument: {0}",
|
||||||
|
"certificate type: " + certType);
|
||||||
}
|
}
|
||||||
|
|
||||||
CreateSignature createSignature = new CreateSignature(ks, password.toCharArray());
|
CreateSignature createSignature = new CreateSignature(ks, password.toCharArray());
|
||||||
|
|||||||
+45
-20
@@ -63,6 +63,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
|
|
||||||
import stirling.software.common.model.api.PDFFile;
|
import stirling.software.common.model.api.PDFFile;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -149,23 +150,39 @@ public class GetInfoOnPDF {
|
|||||||
try {
|
try {
|
||||||
PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata();
|
PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata();
|
||||||
if (pdMetadata != null) {
|
if (pdMetadata != null) {
|
||||||
COSInputStream metaStream = pdMetadata.createInputStream();
|
try (COSInputStream metaStream = pdMetadata.createInputStream()) {
|
||||||
DomXmpParser domXmpParser = new DomXmpParser();
|
// First try to read raw metadata as string to check for standard keywords
|
||||||
XMPMetadata xmpMeta = domXmpParser.parse(metaStream);
|
byte[] metadataBytes = metaStream.readAllBytes();
|
||||||
|
String rawMetadata = new String(metadataBytes, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
if (rawMetadata.contains(standardKeyword)) {
|
||||||
new XmpSerializer().serialize(xmpMeta, baos, true);
|
return true;
|
||||||
String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (xmpString.contains(standardKeyword)) {
|
// If raw check doesn't find it, try parsing with XMP parser
|
||||||
return true;
|
try (COSInputStream metaStream = pdMetadata.createInputStream()) {
|
||||||
|
try {
|
||||||
|
DomXmpParser domXmpParser = new DomXmpParser();
|
||||||
|
XMPMetadata xmpMeta = domXmpParser.parse(metaStream);
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
new XmpSerializer().serialize(xmpMeta, baos, true);
|
||||||
|
String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
if (xmpString.contains(standardKeyword)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (XmpParsingException e) {
|
||||||
|
// XMP parsing failed, but we already checked raw metadata above
|
||||||
|
log.debug(
|
||||||
|
"XMP parsing failed for standard check, but raw metadata was already checked: {}",
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (
|
} catch (Exception e) {
|
||||||
Exception
|
ExceptionUtils.logException("PDF standard checking", e);
|
||||||
e) { // Catching general exception for brevity, ideally you'd catch specific
|
|
||||||
// exceptions.
|
|
||||||
log.error("exception", e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -391,14 +408,22 @@ public class GetInfoOnPDF {
|
|||||||
|
|
||||||
if (pdMetadata != null) {
|
if (pdMetadata != null) {
|
||||||
try {
|
try {
|
||||||
COSInputStream is = pdMetadata.createInputStream();
|
try (COSInputStream is = pdMetadata.createInputStream()) {
|
||||||
DomXmpParser domXmpParser = new DomXmpParser();
|
DomXmpParser domXmpParser = new DomXmpParser();
|
||||||
XMPMetadata xmpMeta = domXmpParser.parse(is);
|
XMPMetadata xmpMeta = domXmpParser.parse(is);
|
||||||
|
|
||||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
new XmpSerializer().serialize(xmpMeta, os, true);
|
new XmpSerializer().serialize(xmpMeta, os, true);
|
||||||
xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8);
|
xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8);
|
||||||
} catch (XmpParsingException | IOException e) {
|
}
|
||||||
|
} catch (XmpParsingException e) {
|
||||||
|
// XMP parsing failed, try to read raw metadata instead
|
||||||
|
log.debug("XMP parsing failed, reading raw metadata: {}", e.getMessage());
|
||||||
|
try (COSInputStream is = pdMetadata.createInputStream()) {
|
||||||
|
byte[] metadataBytes = is.readAllBytes();
|
||||||
|
xmpString = new String(metadataBytes, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
log.error("exception", e);
|
log.error("exception", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-6
@@ -21,6 +21,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import stirling.software.SPDF.model.api.security.AddPasswordRequest;
|
import stirling.software.SPDF.model.api.security.AddPasswordRequest;
|
||||||
import stirling.software.SPDF.model.api.security.PDFPasswordRequest;
|
import stirling.software.SPDF.model.api.security.PDFPasswordRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -42,12 +43,19 @@ public class PasswordController {
|
|||||||
MultipartFile fileInput = request.getFileInput();
|
MultipartFile fileInput = request.getFileInput();
|
||||||
String password = request.getPassword();
|
String password = request.getPassword();
|
||||||
PDDocument document = pdfDocumentFactory.load(fileInput, password);
|
PDDocument document = pdfDocumentFactory.load(fileInput, password);
|
||||||
document.setAllSecurityToBeRemoved(true);
|
|
||||||
return WebResponseUtils.pdfDocToWebResponse(
|
try {
|
||||||
document,
|
document.setAllSecurityToBeRemoved(true);
|
||||||
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
.replaceFirst("[.][^.]+$", "")
|
document,
|
||||||
+ "_password_removed.pdf");
|
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_password_removed.pdf");
|
||||||
|
} catch (IOException e) {
|
||||||
|
document.close();
|
||||||
|
ExceptionUtils.logException("password removal", e);
|
||||||
|
throw ExceptionUtils.handlePdfException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/add-password")
|
@PostMapping(consumes = "multipart/form-data", value = "/add-password")
|
||||||
|
|||||||
+7
-1
@@ -41,6 +41,7 @@ import stirling.software.SPDF.model.api.security.SignatureValidationRequest;
|
|||||||
import stirling.software.SPDF.model.api.security.SignatureValidationResult;
|
import stirling.software.SPDF.model.api.security.SignatureValidationResult;
|
||||||
import stirling.software.SPDF.service.CertificateValidationService;
|
import stirling.software.SPDF.service.CertificateValidationService;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/security")
|
@RequestMapping("/api/v1/security")
|
||||||
@@ -82,7 +83,12 @@ public class ValidateSignatureController {
|
|||||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||||
customCert = (X509Certificate) cf.generateCertificate(certStream);
|
customCert = (X509Certificate) cf.generateCertificate(certStream);
|
||||||
} catch (CertificateException e) {
|
} catch (CertificateException e) {
|
||||||
throw new RuntimeException("Invalid certificate file: " + e.getMessage());
|
throw ExceptionUtils.createRuntimeException(
|
||||||
|
"error.invalidFormat",
|
||||||
|
"Invalid {0} format: {1}",
|
||||||
|
e,
|
||||||
|
"certificate file",
|
||||||
|
e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -29,6 +29,7 @@ import stirling.software.SPDF.service.SignatureService;
|
|||||||
import stirling.software.common.configuration.InstallationPathConfig;
|
import stirling.software.common.configuration.InstallationPathConfig;
|
||||||
import stirling.software.common.configuration.RuntimePathConfig;
|
import stirling.software.common.configuration.RuntimePathConfig;
|
||||||
import stirling.software.common.service.UserServiceInterface;
|
import stirling.software.common.service.UserServiceInterface;
|
||||||
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.GeneralUtils;
|
import stirling.software.common.util.GeneralUtils;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@@ -263,13 +264,17 @@ public class GeneralWebController {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Error processing filename", e);
|
throw ExceptionUtils.createRuntimeException(
|
||||||
|
"error.fontLoadingFailed",
|
||||||
|
"Error processing font file",
|
||||||
|
e);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.toList();
|
.toList();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Failed to read font directory from " + locationPattern, e);
|
throw ExceptionUtils.createRuntimeException(
|
||||||
|
"error.fontDirectoryReadFailed", "Failed to read font directory", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
@@ -19,6 +19,18 @@ public class ProcessPdfWithOcrRequest extends PDFFile {
|
|||||||
defaultValue = "[\"eng\"]")
|
defaultValue = "[\"eng\"]")
|
||||||
private List<String> languages;
|
private List<String> languages;
|
||||||
|
|
||||||
|
@Schema(description = "Include OCR text in a sidecar text file if set to true")
|
||||||
|
private boolean sidecar;
|
||||||
|
|
||||||
|
@Schema(description = "Deskew the input file if set to true")
|
||||||
|
private boolean deskew;
|
||||||
|
|
||||||
|
@Schema(description = "Clean the input file if set to true")
|
||||||
|
private boolean clean;
|
||||||
|
|
||||||
|
@Schema(description = "Clean the final output if set to true")
|
||||||
|
private boolean cleanFinal;
|
||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
description = "Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'",
|
description = "Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'",
|
||||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
@@ -31,4 +43,7 @@ public class ProcessPdfWithOcrRequest extends PDFFile {
|
|||||||
allowableValues = {"hocr", "sandwich"},
|
allowableValues = {"hocr", "sandwich"},
|
||||||
defaultValue = "hocr")
|
defaultValue = "hocr")
|
||||||
private String ocrRenderType = "hocr";
|
private String ocrRenderType = "hocr";
|
||||||
|
|
||||||
|
@Schema(description = "Remove images from the output PDF if set to true")
|
||||||
|
private boolean removeImagesAfter;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,8 +120,8 @@ public class ApiDocService {
|
|||||||
ObjectMapper mapper = new ObjectMapper();
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
apiDocsJsonRootNode = mapper.readTree(apiDocsJson);
|
apiDocsJsonRootNode = mapper.readTree(apiDocsJson);
|
||||||
JsonNode paths = apiDocsJsonRootNode.path("paths");
|
JsonNode paths = apiDocsJsonRootNode.path("paths");
|
||||||
paths.fields()
|
paths.propertyStream()
|
||||||
.forEachRemaining(
|
.forEach(
|
||||||
entry -> {
|
entry -> {
|
||||||
String path = entry.getKey();
|
String path = entry.getKey();
|
||||||
JsonNode pathNode = entry.getValue();
|
JsonNode pathNode = entry.getValue();
|
||||||
|
|||||||
+1
-1
@@ -77,7 +77,7 @@ public class MetricsAggregatorService {
|
|||||||
double lastCount = lastSentMetrics.getOrDefault(key, 0.0);
|
double lastCount = lastSentMetrics.getOrDefault(key, 0.0);
|
||||||
double difference = currentCount - lastCount;
|
double difference = currentCount - lastCount;
|
||||||
if (difference > 0) {
|
if (difference > 0) {
|
||||||
logger.info("{}, {}", key, difference);
|
logger.debug("{}, {}", key, difference);
|
||||||
metrics.put(key, difference);
|
metrics.put(key, difference);
|
||||||
lastSentMetrics.put(key, currentCount);
|
lastSentMetrics.put(key, currentCount);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package stirling.software.common.controller;
|
package stirling.software.common.controller;
|
||||||
|
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -14,6 +18,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.common.model.job.JobResult;
|
import stirling.software.common.model.job.JobResult;
|
||||||
|
import stirling.software.common.model.job.ResultFile;
|
||||||
import stirling.software.common.service.FileStorage;
|
import stirling.software.common.service.FileStorage;
|
||||||
import stirling.software.common.service.JobQueue;
|
import stirling.software.common.service.JobQueue;
|
||||||
import stirling.software.common.service.TaskManager;
|
import stirling.software.common.service.TaskManager;
|
||||||
@@ -78,16 +83,31 @@ public class JobController {
|
|||||||
return ResponseEntity.badRequest().body("Job failed: " + result.getError());
|
return ResponseEntity.badRequest().body("Job failed: " + result.getError());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.getFileId() != null) {
|
// Handle multiple files - return metadata for client to download individually
|
||||||
|
if (result.hasMultipleFiles()) {
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body(
|
||||||
|
Map.of(
|
||||||
|
"jobId",
|
||||||
|
jobId,
|
||||||
|
"hasMultipleFiles",
|
||||||
|
true,
|
||||||
|
"files",
|
||||||
|
result.getAllResultFiles()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle single file (download directly)
|
||||||
|
if (result.hasFiles() && !result.hasMultipleFiles()) {
|
||||||
try {
|
try {
|
||||||
byte[] fileContent = fileStorage.retrieveBytes(result.getFileId());
|
List<ResultFile> files = result.getAllResultFiles();
|
||||||
|
ResultFile singleFile = files.get(0);
|
||||||
|
byte[] fileContent = fileStorage.retrieveBytes(singleFile.getFileId());
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.header("Content-Type", result.getContentType())
|
.header("Content-Type", singleFile.getContentType())
|
||||||
.header(
|
.header(
|
||||||
"Content-Disposition",
|
"Content-Disposition",
|
||||||
"form-data; name=\"attachment\"; filename=\""
|
createContentDispositionHeader(singleFile.getFileName()))
|
||||||
+ result.getOriginalFileName()
|
|
||||||
+ "\"")
|
|
||||||
.body(fileContent);
|
.body(fileContent);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error retrieving file for job {}: {}", jobId, e.getMessage(), e);
|
log.error("Error retrieving file for job {}: {}", jobId, e.getMessage(), e);
|
||||||
@@ -170,4 +190,127 @@ public class JobController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of files for a job
|
||||||
|
*
|
||||||
|
* @param jobId The job ID
|
||||||
|
* @return List of files for the job
|
||||||
|
*/
|
||||||
|
@GetMapping("/api/v1/general/job/{jobId}/result/files")
|
||||||
|
public ResponseEntity<?> getJobFiles(@PathVariable("jobId") String jobId) {
|
||||||
|
JobResult result = taskManager.getJobResult(jobId);
|
||||||
|
if (result == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.isComplete()) {
|
||||||
|
return ResponseEntity.badRequest().body("Job is not complete yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.getError() != null) {
|
||||||
|
return ResponseEntity.badRequest().body("Job failed: " + result.getError());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ResultFile> files = result.getAllResultFiles();
|
||||||
|
return ResponseEntity.ok(
|
||||||
|
Map.of(
|
||||||
|
"jobId", jobId,
|
||||||
|
"fileCount", files.size(),
|
||||||
|
"files", files));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get metadata for an individual file by its file ID
|
||||||
|
*
|
||||||
|
* @param fileId The file ID
|
||||||
|
* @return The file metadata
|
||||||
|
*/
|
||||||
|
@GetMapping("/api/v1/general/files/{fileId}/metadata")
|
||||||
|
public ResponseEntity<?> getFileMetadata(@PathVariable("fileId") String fileId) {
|
||||||
|
try {
|
||||||
|
// Verify file exists
|
||||||
|
if (!fileStorage.fileExists(fileId)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the file metadata from any job that contains this file
|
||||||
|
ResultFile resultFile = taskManager.findResultFileByFileId(fileId);
|
||||||
|
|
||||||
|
if (resultFile != null) {
|
||||||
|
return ResponseEntity.ok(resultFile);
|
||||||
|
} else {
|
||||||
|
// File exists but no metadata found, get basic info efficiently
|
||||||
|
long fileSize = fileStorage.getFileSize(fileId);
|
||||||
|
return ResponseEntity.ok(
|
||||||
|
Map.of(
|
||||||
|
"fileId",
|
||||||
|
fileId,
|
||||||
|
"fileName",
|
||||||
|
"unknown",
|
||||||
|
"contentType",
|
||||||
|
"application/octet-stream",
|
||||||
|
"fileSize",
|
||||||
|
fileSize));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error retrieving file metadata {}: {}", fileId, e.getMessage(), e);
|
||||||
|
return ResponseEntity.internalServerError()
|
||||||
|
.body("Error retrieving file metadata: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download an individual file by its file ID
|
||||||
|
*
|
||||||
|
* @param fileId The file ID
|
||||||
|
* @return The file content
|
||||||
|
*/
|
||||||
|
@GetMapping("/api/v1/general/files/{fileId}")
|
||||||
|
public ResponseEntity<?> downloadFile(@PathVariable("fileId") String fileId) {
|
||||||
|
try {
|
||||||
|
// Verify file exists
|
||||||
|
if (!fileStorage.fileExists(fileId)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve file content
|
||||||
|
byte[] fileContent = fileStorage.retrieveBytes(fileId);
|
||||||
|
|
||||||
|
// Find the file metadata from any job that contains this file
|
||||||
|
// This is for getting the original filename and content type
|
||||||
|
ResultFile resultFile = taskManager.findResultFileByFileId(fileId);
|
||||||
|
|
||||||
|
String fileName = resultFile != null ? resultFile.getFileName() : "download";
|
||||||
|
String contentType =
|
||||||
|
resultFile != null ? resultFile.getContentType() : "application/octet-stream";
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header("Content-Type", contentType)
|
||||||
|
.header("Content-Disposition", createContentDispositionHeader(fileName))
|
||||||
|
.body(fileContent);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error retrieving file {}: {}", fileId, e.getMessage(), e);
|
||||||
|
return ResponseEntity.internalServerError()
|
||||||
|
.body("Error retrieving file: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Content-Disposition header with UTF-8 filename support
|
||||||
|
*
|
||||||
|
* @param fileName The filename to encode
|
||||||
|
* @return Content-Disposition header value
|
||||||
|
*/
|
||||||
|
private String createContentDispositionHeader(String fileName) {
|
||||||
|
try {
|
||||||
|
String encodedFileName =
|
||||||
|
URLEncoder.encode(fileName, StandardCharsets.UTF_8)
|
||||||
|
.replace("+", "%20"); // URLEncoder uses + for spaces, but we want %20
|
||||||
|
return "attachment; filename=\"" + fileName + "\"; filename*=UTF-8''" + encodedFileName;
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Fallback to basic filename if encoding fails
|
||||||
|
return "attachment; filename=\"" + fileName + "\"";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,13 +163,74 @@ alphabet=الأبجدية
|
|||||||
downloadPdf=تنزيل PDF
|
downloadPdf=تنزيل PDF
|
||||||
text=نص
|
text=نص
|
||||||
font=الخط
|
font=الخط
|
||||||
selectFillter=- حدد -
|
selectFilter=- حدد -
|
||||||
pageNum=رقم الصفحة
|
pageNum=رقم الصفحة
|
||||||
sizes.small=صغير
|
sizes.small=صغير
|
||||||
sizes.medium=وسط
|
sizes.medium=وسط
|
||||||
sizes.large=كبير
|
sizes.large=كبير
|
||||||
sizes.x-large=كبير جدا
|
sizes.x-large=كبير جدا
|
||||||
error.pdfPassword=ملف PDF محمي بكلمة مرور ولم يتم تقديم كلمة المرور أو كانت غير صحيحة
|
error.pdfPassword=ملف PDF محمي بكلمة مرور ولم يتم تقديم كلمة المرور أو كانت غير صحيحة
|
||||||
|
error.pdfCorrupted=PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.
|
||||||
|
error.pdfCorruptedMultiple=One or more PDF files appear to be corrupted or damaged. Please try using the 'Repair PDF' feature on each file first before attempting to merge them.
|
||||||
|
error.pdfCorruptedDuring=Error {0}: PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.
|
||||||
|
|
||||||
|
# Frontend corruption error messages
|
||||||
|
error.pdfInvalid=The PDF file "{0}" appears to be corrupted or has an invalid structure. Please try using the 'Repair PDF' feature to fix the file before proceeding.
|
||||||
|
error.tryRepair=Try using the Repair PDF feature to fix corrupted files.
|
||||||
|
|
||||||
|
# Additional error messages
|
||||||
|
error.pdfEncryption=The PDF appears to have corrupted encryption data. This can happen when the PDF was created with incompatible encryption methods. Please try using the 'Repair PDF' feature first, or contact the document creator for a new copy.
|
||||||
|
error.fileProcessing=An error occurred while processing the file during {0} operation: {1}
|
||||||
|
|
||||||
|
# Generic error message templates
|
||||||
|
error.toolNotInstalled={0} is not installed
|
||||||
|
error.toolRequired={0} is required for {1}
|
||||||
|
error.conversionFailed={0} conversion failed
|
||||||
|
error.commandFailed={0} command failed
|
||||||
|
error.algorithmNotAvailable={0} algorithm not available
|
||||||
|
error.optionsNotSpecified={0} options are not specified
|
||||||
|
error.fileFormatRequired=File must be in {0} format
|
||||||
|
error.invalidFormat=Invalid {0} format: {1}
|
||||||
|
error.endpointDisabled=This endpoint has been disabled by the admin
|
||||||
|
error.urlNotReachable=URL is not reachable, please provide a valid URL
|
||||||
|
|
||||||
|
# DPI and image rendering messages - used by frontend for dynamic translation
|
||||||
|
# Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message
|
||||||
|
# Frontend parses this and replaces with localized versions using these keys
|
||||||
|
error.dpiExceedsLimit=DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause memory issues and crashes. Please use a lower DPI value.
|
||||||
|
error.pageTooBigForDpi=PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).
|
||||||
|
error.pageTooBigExceedsArray=PDF page {0} is too large to render at {1} DPI. The resulting image would exceed Java's maximum array size. Please try a lower DPI value (recommended: 150 or less).
|
||||||
|
error.pageTooBigFor300Dpi=PDF page {0} is too large to render at 300 DPI. The resulting image would exceed Java's maximum array size. Please use a lower DPI value for PDF-to-image conversion.
|
||||||
|
|
||||||
|
# URL and website conversion messages
|
||||||
|
|
||||||
|
# System requirements messages
|
||||||
|
|
||||||
|
# Authentication and security messages
|
||||||
|
error.apiKeyInvalid=API key is not valid.
|
||||||
|
error.userNotFound=User not found.
|
||||||
|
error.passwordRequired=Password must not be null.
|
||||||
|
error.accountLocked=Your account has been locked due to too many failed login attempts.
|
||||||
|
error.invalidEmail=Invalid email addresses provided.
|
||||||
|
error.emailAttachmentRequired=An attachment is required to send the email.
|
||||||
|
error.signatureNotFound=Signature file not found.
|
||||||
|
|
||||||
|
# File processing messages
|
||||||
|
error.fileNotFound=File not found with ID: {0}
|
||||||
|
|
||||||
|
# Database and configuration messages
|
||||||
|
error.noBackupScripts=No backup scripts were found.
|
||||||
|
error.unsupportedProvider={0} is not currently supported.
|
||||||
|
error.pathTraversalDetected=Path traversal detected for security reasons.
|
||||||
|
|
||||||
|
# Validation messages
|
||||||
|
error.invalidArgument=Invalid argument: {0}
|
||||||
|
error.argumentRequired={0} must not be null
|
||||||
|
error.operationFailed=Operation failed: {0}
|
||||||
|
error.angleNotMultipleOf90=Angle must be a multiple of 90
|
||||||
|
error.pdfBookmarksNotFound=No PDF bookmarks/outline found in document
|
||||||
|
error.fontLoadingFailed=Error processing font file
|
||||||
|
error.fontDirectoryReadFailed=Failed to read font directory
|
||||||
delete=حذف
|
delete=حذف
|
||||||
username=اسم المستخدم
|
username=اسم المستخدم
|
||||||
password=كلمة المرور
|
password=كلمة المرور
|
||||||
@@ -181,7 +242,6 @@ red=أحمر
|
|||||||
green=أخضر
|
green=أخضر
|
||||||
blue=أزرق
|
blue=أزرق
|
||||||
custom=مخصص...
|
custom=مخصص...
|
||||||
WorkInProgess=العمل قيد التقدم، قد لا يعمل أو يحتوي على أخطاء، يرجى الإبلاغ عن أي مشاكل!
|
|
||||||
poweredBy=مدعوم بواسطة
|
poweredBy=مدعوم بواسطة
|
||||||
yes=نعم
|
yes=نعم
|
||||||
no=لا
|
no=لا
|
||||||
@@ -525,6 +585,10 @@ home.addImage.title=إضافة صورة إلى ملف PDF
|
|||||||
home.addImage.desc=إضافة صورة إلى موقع معين في PDF (العمل قيد التقدم)
|
home.addImage.desc=إضافة صورة إلى موقع معين في PDF (العمل قيد التقدم)
|
||||||
addImage.tags=صورة,jpg,صورة,صورة فوتوغرافية
|
addImage.tags=صورة,jpg,صورة,صورة فوتوغرافية
|
||||||
|
|
||||||
|
home.attachments.title=Add Attachments
|
||||||
|
home.attachments.desc=Add or remove embedded files (attachments) to/from a PDF
|
||||||
|
attachments.tags=embed,attach,file,attachment,attachments
|
||||||
|
|
||||||
home.watermark.title=إضافة علامة مائية
|
home.watermark.title=إضافة علامة مائية
|
||||||
home.watermark.desc=أضف علامة مائية مخصصة إلى مستند PDF الخاص بك.
|
home.watermark.desc=أضف علامة مائية مخصصة إلى مستند PDF الخاص بك.
|
||||||
watermark.tags=نص,تكرار,تسمية,خاص,حقوق النشر,علامة تجارية,صورة,jpg,صورة,صورة فوتوغرافية
|
watermark.tags=نص,تكرار,تسمية,خاص,حقوق النشر,علامة تجارية,صورة,jpg,صورة,صورة فوتوغرافية
|
||||||
@@ -1205,6 +1269,12 @@ addImage.everyPage=كل صفحة؟
|
|||||||
addImage.upload=إضافة صورة
|
addImage.upload=إضافة صورة
|
||||||
addImage.submit=إضافة صورة
|
addImage.submit=إضافة صورة
|
||||||
|
|
||||||
|
#attachments
|
||||||
|
attachments.title=Add Attachments
|
||||||
|
attachments.header=Add attachments
|
||||||
|
attachments.description=Allows you to add attachments to the PDF
|
||||||
|
attachments.descriptionPlaceholder=Enter a description for the attachments...
|
||||||
|
attachments.addButton=Add Attachments
|
||||||
|
|
||||||
#merge
|
#merge
|
||||||
merge.title=دمج
|
merge.title=دمج
|
||||||
@@ -1594,6 +1664,7 @@ fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
|||||||
fileChooser.dragAndDropImage=Drag & Drop Image file
|
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||||
fileChooser.hoveredDragAndDrop=قم بسحب المفات وإفلاتها هنا
|
fileChooser.hoveredDragAndDrop=قم بسحب المفات وإفلاتها هنا
|
||||||
fileChooser.extractPDF=جاري الاستخراج...
|
fileChooser.extractPDF=جاري الاستخراج...
|
||||||
|
fileChooser.addAttachments=drag & drop attachments here
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
releases.footer=Releases
|
releases.footer=Releases
|
||||||
|
|||||||
@@ -163,13 +163,74 @@ alphabet=Əlifba
|
|||||||
downloadPdf=PDF Yüklə
|
downloadPdf=PDF Yüklə
|
||||||
text=Yazı
|
text=Yazı
|
||||||
font=Şrift
|
font=Şrift
|
||||||
selectFillter=-- Seç --
|
selectFilter=-- Seç --
|
||||||
pageNum=Səhifə nömrəsi
|
pageNum=Səhifə nömrəsi
|
||||||
sizes.small=Kiçik
|
sizes.small=Kiçik
|
||||||
sizes.medium=Orta
|
sizes.medium=Orta
|
||||||
sizes.large=Böyük
|
sizes.large=Böyük
|
||||||
sizes.x-large=Ekstra Böyük
|
sizes.x-large=Ekstra Böyük
|
||||||
error.pdfPassword=PDF sənədi şifrlənmişdir və şifr təmin edilməmişdir və ya yanlışdır.
|
error.pdfPassword=PDF sənədi şifrlənmişdir və şifr təmin edilməmişdir və ya yanlışdır.
|
||||||
|
error.pdfCorrupted=PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.
|
||||||
|
error.pdfCorruptedMultiple=One or more PDF files appear to be corrupted or damaged. Please try using the 'Repair PDF' feature on each file first before attempting to merge them.
|
||||||
|
error.pdfCorruptedDuring=Error {0}: PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.
|
||||||
|
|
||||||
|
# Frontend corruption error messages
|
||||||
|
error.pdfInvalid=The PDF file "{0}" appears to be corrupted or has an invalid structure. Please try using the 'Repair PDF' feature to fix the file before proceeding.
|
||||||
|
error.tryRepair=Try using the Repair PDF feature to fix corrupted files.
|
||||||
|
|
||||||
|
# Additional error messages
|
||||||
|
error.pdfEncryption=The PDF appears to have corrupted encryption data. This can happen when the PDF was created with incompatible encryption methods. Please try using the 'Repair PDF' feature first, or contact the document creator for a new copy.
|
||||||
|
error.fileProcessing=An error occurred while processing the file during {0} operation: {1}
|
||||||
|
|
||||||
|
# Generic error message templates
|
||||||
|
error.toolNotInstalled={0} is not installed
|
||||||
|
error.toolRequired={0} is required for {1}
|
||||||
|
error.conversionFailed={0} conversion failed
|
||||||
|
error.commandFailed={0} command failed
|
||||||
|
error.algorithmNotAvailable={0} algorithm not available
|
||||||
|
error.optionsNotSpecified={0} options are not specified
|
||||||
|
error.fileFormatRequired=File must be in {0} format
|
||||||
|
error.invalidFormat=Invalid {0} format: {1}
|
||||||
|
error.endpointDisabled=This endpoint has been disabled by the admin
|
||||||
|
error.urlNotReachable=URL is not reachable, please provide a valid URL
|
||||||
|
|
||||||
|
# DPI and image rendering messages - used by frontend for dynamic translation
|
||||||
|
# Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message
|
||||||
|
# Frontend parses this and replaces with localized versions using these keys
|
||||||
|
error.dpiExceedsLimit=DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause memory issues and crashes. Please use a lower DPI value.
|
||||||
|
error.pageTooBigForDpi=PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).
|
||||||
|
error.pageTooBigExceedsArray=PDF page {0} is too large to render at {1} DPI. The resulting image would exceed Java's maximum array size. Please try a lower DPI value (recommended: 150 or less).
|
||||||
|
error.pageTooBigFor300Dpi=PDF page {0} is too large to render at 300 DPI. The resulting image would exceed Java's maximum array size. Please use a lower DPI value for PDF-to-image conversion.
|
||||||
|
|
||||||
|
# URL and website conversion messages
|
||||||
|
|
||||||
|
# System requirements messages
|
||||||
|
|
||||||
|
# Authentication and security messages
|
||||||
|
error.apiKeyInvalid=API key is not valid.
|
||||||
|
error.userNotFound=User not found.
|
||||||
|
error.passwordRequired=Password must not be null.
|
||||||
|
error.accountLocked=Your account has been locked due to too many failed login attempts.
|
||||||
|
error.invalidEmail=Invalid email addresses provided.
|
||||||
|
error.emailAttachmentRequired=An attachment is required to send the email.
|
||||||
|
error.signatureNotFound=Signature file not found.
|
||||||
|
|
||||||
|
# File processing messages
|
||||||
|
error.fileNotFound=File not found with ID: {0}
|
||||||
|
|
||||||
|
# Database and configuration messages
|
||||||
|
error.noBackupScripts=No backup scripts were found.
|
||||||
|
error.unsupportedProvider={0} is not currently supported.
|
||||||
|
error.pathTraversalDetected=Path traversal detected for security reasons.
|
||||||
|
|
||||||
|
# Validation messages
|
||||||
|
error.invalidArgument=Invalid argument: {0}
|
||||||
|
error.argumentRequired={0} must not be null
|
||||||
|
error.operationFailed=Operation failed: {0}
|
||||||
|
error.angleNotMultipleOf90=Angle must be a multiple of 90
|
||||||
|
error.pdfBookmarksNotFound=No PDF bookmarks/outline found in document
|
||||||
|
error.fontLoadingFailed=Error processing font file
|
||||||
|
error.fontDirectoryReadFailed=Failed to read font directory
|
||||||
delete=Sil
|
delete=Sil
|
||||||
username=İstifadəçi Adı
|
username=İstifadəçi Adı
|
||||||
password=Şifr
|
password=Şifr
|
||||||
@@ -181,7 +242,6 @@ red=Qırmızı
|
|||||||
green=Yaşıl
|
green=Yaşıl
|
||||||
blue=Mavi
|
blue=Mavi
|
||||||
custom=Xüsusi...
|
custom=Xüsusi...
|
||||||
WorkInProgess=İş davam edir, İşləməyə bilər və ya xətalarla üzləşə bilərsiniz, Zəhmət olmasa problemləri bildirin!
|
|
||||||
poweredBy=Təchiz edilmişdir
|
poweredBy=Təchiz edilmişdir
|
||||||
yes=Bəli
|
yes=Bəli
|
||||||
no=Xeyr
|
no=Xeyr
|
||||||
@@ -525,6 +585,10 @@ home.addImage.title=Şəkil əlavə et
|
|||||||
home.addImage.desc=PDF-də təyin edilmiş yerə şəkil əlavə edir
|
home.addImage.desc=PDF-də təyin edilmiş yerə şəkil əlavə edir
|
||||||
addImage.tags=şəkil,jpg,fotoşəkil,foto
|
addImage.tags=şəkil,jpg,fotoşəkil,foto
|
||||||
|
|
||||||
|
home.attachments.title=Add Attachments
|
||||||
|
home.attachments.desc=Add or remove embedded files (attachments) to/from a PDF
|
||||||
|
attachments.tags=embed,attach,file,attachment,attachments
|
||||||
|
|
||||||
home.watermark.title=Watermark əlavə et
|
home.watermark.title=Watermark əlavə et
|
||||||
home.watermark.desc=PDF sənədinə fərdi watermark əlavə et.
|
home.watermark.desc=PDF sənədinə fərdi watermark əlavə et.
|
||||||
watermark.tags=Mətn,təkrarlanan,nişan,sahib olmaq,müəllif hüquqları,əmtəə nişanı,şəkil,jpg,fotoşəkil,foto
|
watermark.tags=Mətn,təkrarlanan,nişan,sahib olmaq,müəllif hüquqları,əmtəə nişanı,şəkil,jpg,fotoşəkil,foto
|
||||||
@@ -1205,6 +1269,12 @@ addImage.everyPage=Bütün Səhifələr?
|
|||||||
addImage.upload=Şəkli Əlavə Et
|
addImage.upload=Şəkli Əlavə Et
|
||||||
addImage.submit=Şəkli Əlavə Et
|
addImage.submit=Şəkli Əlavə Et
|
||||||
|
|
||||||
|
#attachments
|
||||||
|
attachments.title=Add Attachments
|
||||||
|
attachments.header=Add attachments
|
||||||
|
attachments.description=Allows you to add attachments to the PDF
|
||||||
|
attachments.descriptionPlaceholder=Enter a description for the attachments...
|
||||||
|
attachments.addButton=Add Attachments
|
||||||
|
|
||||||
#merge
|
#merge
|
||||||
merge.title=Birləşdirin
|
merge.title=Birləşdirin
|
||||||
@@ -1594,6 +1664,7 @@ fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
|||||||
fileChooser.dragAndDropImage=Drag & Drop Image file
|
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||||
fileChooser.extractPDF=Extracting...
|
fileChooser.extractPDF=Extracting...
|
||||||
|
fileChooser.addAttachments=drag & drop attachments here
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
releases.footer=Buraxılışlar
|
releases.footer=Buraxılışlar
|
||||||
|
|||||||
@@ -163,13 +163,74 @@ alphabet=Азбука
|
|||||||
downloadPdf=Изтеглете PDF
|
downloadPdf=Изтеглете PDF
|
||||||
text=Текст
|
text=Текст
|
||||||
font=Шрифт
|
font=Шрифт
|
||||||
selectFillter=-- Изберете --
|
selectFilter=-- Изберете --
|
||||||
pageNum=Брой страница
|
pageNum=Брой страница
|
||||||
sizes.small=Малък
|
sizes.small=Малък
|
||||||
sizes.medium=Среден
|
sizes.medium=Среден
|
||||||
sizes.large=Голям
|
sizes.large=Голям
|
||||||
sizes.x-large=X-Голям
|
sizes.x-large=X-Голям
|
||||||
error.pdfPassword=PDF документът е с парола и или паролата не е предоставена, или е неправилна
|
error.pdfPassword=PDF документът е с парола и или паролата не е предоставена, или е неправилна
|
||||||
|
error.pdfCorrupted=PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.
|
||||||
|
error.pdfCorruptedMultiple=One or more PDF files appear to be corrupted or damaged. Please try using the 'Repair PDF' feature on each file first before attempting to merge them.
|
||||||
|
error.pdfCorruptedDuring=Error {0}: PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.
|
||||||
|
|
||||||
|
# Frontend corruption error messages
|
||||||
|
error.pdfInvalid=The PDF file "{0}" appears to be corrupted or has an invalid structure. Please try using the 'Repair PDF' feature to fix the file before proceeding.
|
||||||
|
error.tryRepair=Try using the Repair PDF feature to fix corrupted files.
|
||||||
|
|
||||||
|
# Additional error messages
|
||||||
|
error.pdfEncryption=The PDF appears to have corrupted encryption data. This can happen when the PDF was created with incompatible encryption methods. Please try using the 'Repair PDF' feature first, or contact the document creator for a new copy.
|
||||||
|
error.fileProcessing=An error occurred while processing the file during {0} operation: {1}
|
||||||
|
|
||||||
|
# Generic error message templates
|
||||||
|
error.toolNotInstalled={0} is not installed
|
||||||
|
error.toolRequired={0} is required for {1}
|
||||||
|
error.conversionFailed={0} conversion failed
|
||||||
|
error.commandFailed={0} command failed
|
||||||
|
error.algorithmNotAvailable={0} algorithm not available
|
||||||
|
error.optionsNotSpecified={0} options are not specified
|
||||||
|
error.fileFormatRequired=File must be in {0} format
|
||||||
|
error.invalidFormat=Invalid {0} format: {1}
|
||||||
|
error.endpointDisabled=This endpoint has been disabled by the admin
|
||||||
|
error.urlNotReachable=URL is not reachable, please provide a valid URL
|
||||||
|
|
||||||
|
# DPI and image rendering messages - used by frontend for dynamic translation
|
||||||
|
# Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message
|
||||||
|
# Frontend parses this and replaces with localized versions using these keys
|
||||||
|
error.dpiExceedsLimit=DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause memory issues and crashes. Please use a lower DPI value.
|
||||||
|
error.pageTooBigForDpi=PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).
|
||||||
|
error.pageTooBigExceedsArray=PDF page {0} is too large to render at {1} DPI. The resulting image would exceed Java's maximum array size. Please try a lower DPI value (recommended: 150 or less).
|
||||||
|
error.pageTooBigFor300Dpi=PDF page {0} is too large to render at 300 DPI. The resulting image would exceed Java's maximum array size. Please use a lower DPI value for PDF-to-image conversion.
|
||||||
|
|
||||||
|
# URL and website conversion messages
|
||||||
|
|
||||||
|
# System requirements messages
|
||||||
|
|
||||||
|
# Authentication and security messages
|
||||||
|
error.apiKeyInvalid=API key is not valid.
|
||||||
|
error.userNotFound=User not found.
|
||||||
|
error.passwordRequired=Password must not be null.
|
||||||
|
error.accountLocked=Your account has been locked due to too many failed login attempts.
|
||||||
|
error.invalidEmail=Invalid email addresses provided.
|
||||||
|
error.emailAttachmentRequired=An attachment is required to send the email.
|
||||||
|
error.signatureNotFound=Signature file not found.
|
||||||
|
|
||||||
|
# File processing messages
|
||||||
|
error.fileNotFound=File not found with ID: {0}
|
||||||
|
|
||||||
|
# Database and configuration messages
|
||||||
|
error.noBackupScripts=No backup scripts were found.
|
||||||
|
error.unsupportedProvider={0} is not currently supported.
|
||||||
|
error.pathTraversalDetected=Path traversal detected for security reasons.
|
||||||
|
|
||||||
|
# Validation messages
|
||||||
|
error.invalidArgument=Invalid argument: {0}
|
||||||
|
error.argumentRequired={0} must not be null
|
||||||
|
error.operationFailed=Operation failed: {0}
|
||||||
|
error.angleNotMultipleOf90=Angle must be a multiple of 90
|
||||||
|
error.pdfBookmarksNotFound=No PDF bookmarks/outline found in document
|
||||||
|
error.fontLoadingFailed=Error processing font file
|
||||||
|
error.fontDirectoryReadFailed=Failed to read font directory
|
||||||
delete=Изтрий
|
delete=Изтрий
|
||||||
username=Потребителско име
|
username=Потребителско име
|
||||||
password=Парола
|
password=Парола
|
||||||
@@ -181,7 +242,6 @@ red=Червено
|
|||||||
green=Зелено
|
green=Зелено
|
||||||
blue=Синьо
|
blue=Синьо
|
||||||
custom=Персонализиране...
|
custom=Персонализиране...
|
||||||
WorkInProgess=Работата е в ход, може да не работи или да има грешки, моля, докладвайте за проблеми!
|
|
||||||
poweredBy=Задвижван чрез
|
poweredBy=Задвижван чрез
|
||||||
yes=Да
|
yes=Да
|
||||||
no=Не
|
no=Не
|
||||||
@@ -525,6 +585,10 @@ home.addImage.title=Добавяне на изображение
|
|||||||
home.addImage.desc=Добавя изображение към зададено място към PDF файла
|
home.addImage.desc=Добавя изображение към зададено място към PDF файла
|
||||||
addImage.tags=img,jpg,изображение,снимка
|
addImage.tags=img,jpg,изображение,снимка
|
||||||
|
|
||||||
|
home.attachments.title=Add Attachments
|
||||||
|
home.attachments.desc=Add or remove embedded files (attachments) to/from a PDF
|
||||||
|
attachments.tags=embed,attach,file,attachment,attachments
|
||||||
|
|
||||||
home.watermark.title=Добавяне на воден знак
|
home.watermark.title=Добавяне на воден знак
|
||||||
home.watermark.desc=Добавете персонализиран воден знак към вашия PDF документ.
|
home.watermark.desc=Добавете персонализиран воден знак към вашия PDF документ.
|
||||||
watermark.tags=Текст,повтарящ се,етикет,собствено,авторско право,търговска марка,img,jpg,изображение,снимка
|
watermark.tags=Текст,повтарящ се,етикет,собствено,авторско право,търговска марка,img,jpg,изображение,снимка
|
||||||
@@ -1205,6 +1269,12 @@ addImage.everyPage=Всяка страница?
|
|||||||
addImage.upload=Добавяне на изображение
|
addImage.upload=Добавяне на изображение
|
||||||
addImage.submit=Добавяне на изображение
|
addImage.submit=Добавяне на изображение
|
||||||
|
|
||||||
|
#attachments
|
||||||
|
attachments.title=Add Attachments
|
||||||
|
attachments.header=Add attachments
|
||||||
|
attachments.description=Allows you to add attachments to the PDF
|
||||||
|
attachments.descriptionPlaceholder=Enter a description for the attachments...
|
||||||
|
attachments.addButton=Add Attachments
|
||||||
|
|
||||||
#merge
|
#merge
|
||||||
merge.title=Обединяване
|
merge.title=Обединяване
|
||||||
@@ -1594,6 +1664,7 @@ fileChooser.dragAndDropPDF=Влачете и пуснете PDF файл
|
|||||||
fileChooser.dragAndDropImage=Влачете и пуснете изображение
|
fileChooser.dragAndDropImage=Влачете и пуснете изображение
|
||||||
fileChooser.hoveredDragAndDrop=Влачете и пуснете файл(ове) тук
|
fileChooser.hoveredDragAndDrop=Влачете и пуснете файл(ове) тук
|
||||||
fileChooser.extractPDF=Извличане...
|
fileChooser.extractPDF=Извличане...
|
||||||
|
fileChooser.addAttachments=drag & drop attachments here
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
releases.footer=Версии
|
releases.footer=Версии
|
||||||
|
|||||||
@@ -163,13 +163,74 @@ alphabet=གསལ་བྱེད།
|
|||||||
downloadPdf=PDF ཕབ་ལེན།
|
downloadPdf=PDF ཕབ་ལེན།
|
||||||
text=ཡི་གེ
|
text=ཡི་གེ
|
||||||
font=ཡིག་གཟུགས་ཌྷ
|
font=ཡིག་གཟུགས་ཌྷ
|
||||||
selectFillter=-- འདེམས་རོགས། --
|
selectFilter=-- འདེམས་རོགས། --
|
||||||
pageNum=ཤོག་གིངས།
|
pageNum=ཤོག་གིངས།
|
||||||
sizes.small=ཆུང་ཆང་།
|
sizes.small=ཆུང་ཆང་།
|
||||||
sizes.medium=འབྲིང་ཚད།
|
sizes.medium=འབྲིང་ཚད།
|
||||||
sizes.large=ཆེན་པོ།
|
sizes.large=ཆེན་པོ།
|
||||||
sizes.x-large=ཧ་ཅང་ཆེན་པོ།
|
sizes.x-large=ཧ་ཅང་ཆེན་པོ།
|
||||||
error.pdfPassword=PDF ཡིག་ཆར་གསང་ཚིག་བཀོད་ཡོད་པ་དང་། གསང་ཚིག་མ་བཀོད་པའམ་ནོར་འདུག
|
error.pdfPassword=PDF ཡིག་ཆར་གསང་ཚིག་བཀོད་ཡོད་པ་དང་། གསང་ཚིག་མ་བཀོད་པའམ་ནོར་འདུག
|
||||||
|
error.pdfCorrupted=PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.
|
||||||
|
error.pdfCorruptedMultiple=One or more PDF files appear to be corrupted or damaged. Please try using the 'Repair PDF' feature on each file first before attempting to merge them.
|
||||||
|
error.pdfCorruptedDuring=Error {0}: PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.
|
||||||
|
|
||||||
|
# Frontend corruption error messages
|
||||||
|
error.pdfInvalid=The PDF file "{0}" appears to be corrupted or has an invalid structure. Please try using the 'Repair PDF' feature to fix the file before proceeding.
|
||||||
|
error.tryRepair=Try using the Repair PDF feature to fix corrupted files.
|
||||||
|
|
||||||
|
# Additional error messages
|
||||||
|
error.pdfEncryption=The PDF appears to have corrupted encryption data. This can happen when the PDF was created with incompatible encryption methods. Please try using the 'Repair PDF' feature first, or contact the document creator for a new copy.
|
||||||
|
error.fileProcessing=An error occurred while processing the file during {0} operation: {1}
|
||||||
|
|
||||||
|
# Generic error message templates
|
||||||
|
error.toolNotInstalled={0} is not installed
|
||||||
|
error.toolRequired={0} is required for {1}
|
||||||
|
error.conversionFailed={0} conversion failed
|
||||||
|
error.commandFailed={0} command failed
|
||||||
|
error.algorithmNotAvailable={0} algorithm not available
|
||||||
|
error.optionsNotSpecified={0} options are not specified
|
||||||
|
error.fileFormatRequired=File must be in {0} format
|
||||||
|
error.invalidFormat=Invalid {0} format: {1}
|
||||||
|
error.endpointDisabled=This endpoint has been disabled by the admin
|
||||||
|
error.urlNotReachable=URL is not reachable, please provide a valid URL
|
||||||
|
|
||||||
|
# DPI and image rendering messages - used by frontend for dynamic translation
|
||||||
|
# Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message
|
||||||
|
# Frontend parses this and replaces with localized versions using these keys
|
||||||
|
error.dpiExceedsLimit=DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause memory issues and crashes. Please use a lower DPI value.
|
||||||
|
error.pageTooBigForDpi=PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).
|
||||||
|
error.pageTooBigExceedsArray=PDF page {0} is too large to render at {1} DPI. The resulting image would exceed Java's maximum array size. Please try a lower DPI value (recommended: 150 or less).
|
||||||
|
error.pageTooBigFor300Dpi=PDF page {0} is too large to render at 300 DPI. The resulting image would exceed Java's maximum array size. Please use a lower DPI value for PDF-to-image conversion.
|
||||||
|
|
||||||
|
# URL and website conversion messages
|
||||||
|
|
||||||
|
# System requirements messages
|
||||||
|
|
||||||
|
# Authentication and security messages
|
||||||
|
error.apiKeyInvalid=API key is not valid.
|
||||||
|
error.userNotFound=User not found.
|
||||||
|
error.passwordRequired=Password must not be null.
|
||||||
|
error.accountLocked=Your account has been locked due to too many failed login attempts.
|
||||||
|
error.invalidEmail=Invalid email addresses provided.
|
||||||
|
error.emailAttachmentRequired=An attachment is required to send the email.
|
||||||
|
error.signatureNotFound=Signature file not found.
|
||||||
|
|
||||||
|
# File processing messages
|
||||||
|
error.fileNotFound=File not found with ID: {0}
|
||||||
|
|
||||||
|
# Database and configuration messages
|
||||||
|
error.noBackupScripts=No backup scripts were found.
|
||||||
|
error.unsupportedProvider={0} is not currently supported.
|
||||||
|
error.pathTraversalDetected=Path traversal detected for security reasons.
|
||||||
|
|
||||||
|
# Validation messages
|
||||||
|
error.invalidArgument=Invalid argument: {0}
|
||||||
|
error.argumentRequired={0} must not be null
|
||||||
|
error.operationFailed=Operation failed: {0}
|
||||||
|
error.angleNotMultipleOf90=Angle must be a multiple of 90
|
||||||
|
error.pdfBookmarksNotFound=No PDF bookmarks/outline found in document
|
||||||
|
error.fontLoadingFailed=Error processing font file
|
||||||
|
error.fontDirectoryReadFailed=Failed to read font directory
|
||||||
delete=སུབ་པ།
|
delete=སུབ་པ།
|
||||||
username=སྤྱོད་མཁན་མིང་།
|
username=སྤྱོད་མཁན་མིང་།
|
||||||
password=གསང་ཚིག།
|
password=གསང་ཚིག།
|
||||||
@@ -181,7 +242,6 @@ red=དམར་པོ
|
|||||||
green=ལྗང་ཁུ།
|
green=ལྗང་ཁུ།
|
||||||
blue=སྔོན་པོ
|
blue=སྔོན་པོ
|
||||||
custom=མཚན་ཉིད་རང་སྒྲིག...
|
custom=མཚན་ཉིད་རང་སྒྲིག...
|
||||||
WorkInProgess=ལས་ཀ་བྱེད་བཞིན་པ། ནོར་འཁྲུལ་ཡོང་སྲིད། དཀའ་ངལ་ཡོད་ཚེ་སྙན་སེང་གནང་རོགས།
|
|
||||||
poweredBy=མཁོ་སྲོད་བྱེད་མཁན།
|
poweredBy=མཁོ་སྲོད་བྱེད་མཁན།
|
||||||
yes=ཡིན།
|
yes=ཡིན།
|
||||||
no=མིན།
|
no=མིན།
|
||||||
@@ -525,6 +585,10 @@ home.addImage.title=པར་རིས་སྣོན་པ།
|
|||||||
home.addImage.desc=PDF ནང་གནས་ས་ངེས་ཅན་ཞིག་ཏུ་པར་རིས་སྣོན་པ།
|
home.addImage.desc=PDF ནང་གནས་ས་ངེས་ཅན་ཞིག་ཏུ་པར་རིས་སྣོན་པ།
|
||||||
addImage.tags=པར་རིས།,jpg,པར།,འདྲ་པར།
|
addImage.tags=པར་རིས།,jpg,པར།,འདྲ་པར།
|
||||||
|
|
||||||
|
home.attachments.title=Add Attachments
|
||||||
|
home.attachments.desc=Add or remove embedded files (attachments) to/from a PDF
|
||||||
|
attachments.tags=embed,attach,file,attachment,attachments
|
||||||
|
|
||||||
home.watermark.title=ཆུ་རྟགས་སྣོན་པ།
|
home.watermark.title=ཆུ་རྟགས་སྣོན་པ།
|
||||||
home.watermark.desc=PDF ཡིག་ཆར་རང་སྒྲིག་གི་ཆུ་རྟགས་སྣོན་པ།
|
home.watermark.desc=PDF ཡིག་ཆར་རང་སྒྲིག་གི་ཆུ་རྟགས་སྣོན་པ།
|
||||||
watermark.tags=ཡི་གེ,བསྐྱར་ཟློས།,ཁ་ཡིག,རང་དབང་།,པར་དབང་།,ཚོང་རྟགས།,པར་རིས།,jpg,པར།,འདྲ་པར།
|
watermark.tags=ཡི་གེ,བསྐྱར་ཟློས།,ཁ་ཡིག,རང་དབང་།,པར་དབང་།,ཚོང་རྟགས།,པར་རིས།,jpg,པར།,འདྲ་པར།
|
||||||
@@ -1205,6 +1269,12 @@ addImage.everyPage=ཤོག་ངོས་ཚང་མར་ཡིན་ནམ
|
|||||||
addImage.upload=པར་རིས་སྣོན་པ།
|
addImage.upload=པར་རིས་སྣོན་པ།
|
||||||
addImage.submit=པར་རིས་སྣོན་པ།
|
addImage.submit=པར་རིས་སྣོན་པ།
|
||||||
|
|
||||||
|
#attachments
|
||||||
|
attachments.title=Add Attachments
|
||||||
|
attachments.header=Add attachments
|
||||||
|
attachments.description=Allows you to add attachments to the PDF
|
||||||
|
attachments.descriptionPlaceholder=Enter a description for the attachments...
|
||||||
|
attachments.addButton=Add Attachments
|
||||||
|
|
||||||
#merge
|
#merge
|
||||||
merge.title=སྡེབ་སྦྱོར།
|
merge.title=སྡེབ་སྦྱོར།
|
||||||
@@ -1594,6 +1664,7 @@ fileChooser.dragAndDropPDF=PDF ཡིག་ཆ་འཐེན་ནས་འཇ
|
|||||||
fileChooser.dragAndDropImage=པར་རིས་ཡིག་ཆ་འཐེན་ནས་འཇོག་པ།
|
fileChooser.dragAndDropImage=པར་རིས་ཡིག་ཆ་འཐེན་ནས་འཇོག་པ།
|
||||||
fileChooser.hoveredDragAndDrop=ཡིག་ཆ་འདིར་འཐེན་ནས་འཇོག་པ།
|
fileChooser.hoveredDragAndDrop=ཡིག་ཆ་འདིར་འཐེན་ནས་འཇོག་པ།
|
||||||
fileChooser.extractPDF=འདོན་རིས་འགྱུར་བའི་སྒྲིག་བཏང་བ།
|
fileChooser.extractPDF=འདོན་རིས་འགྱུར་བའི་སྒྲིག་བཏང་བ།
|
||||||
|
fileChooser.addAttachments=drag & drop attachments here
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
releases.footer=པར་གཞི།
|
releases.footer=པར་གཞི།
|
||||||
|
|||||||
@@ -163,13 +163,74 @@ alphabet=Alfabet
|
|||||||
downloadPdf=Descarregueu PDF
|
downloadPdf=Descarregueu PDF
|
||||||
text=Text
|
text=Text
|
||||||
font=Tipus de lletra
|
font=Tipus de lletra
|
||||||
selectFillter=-- Selecciona --
|
selectFilter=-- Selecciona --
|
||||||
pageNum=Número de pàgina
|
pageNum=Número de pàgina
|
||||||
sizes.small=Petit
|
sizes.small=Petit
|
||||||
sizes.medium=Mitjà
|
sizes.medium=Mitjà
|
||||||
sizes.large=Llarg
|
sizes.large=Llarg
|
||||||
sizes.x-large=X-Large
|
sizes.x-large=X-Large
|
||||||
error.pdfPassword=El PDF està protegit o bé el password és incorrecte
|
error.pdfPassword=El PDF està protegit o bé el password és incorrecte
|
||||||
|
error.pdfCorrupted=PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.
|
||||||
|
error.pdfCorruptedMultiple=One or more PDF files appear to be corrupted or damaged. Please try using the 'Repair PDF' feature on each file first before attempting to merge them.
|
||||||
|
error.pdfCorruptedDuring=Error {0}: PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.
|
||||||
|
|
||||||
|
# Frontend corruption error messages
|
||||||
|
error.pdfInvalid=The PDF file "{0}" appears to be corrupted or has an invalid structure. Please try using the 'Repair PDF' feature to fix the file before proceeding.
|
||||||
|
error.tryRepair=Try using the Repair PDF feature to fix corrupted files.
|
||||||
|
|
||||||
|
# Additional error messages
|
||||||
|
error.pdfEncryption=The PDF appears to have corrupted encryption data. This can happen when the PDF was created with incompatible encryption methods. Please try using the 'Repair PDF' feature first, or contact the document creator for a new copy.
|
||||||
|
error.fileProcessing=An error occurred while processing the file during {0} operation: {1}
|
||||||
|
|
||||||
|
# Generic error message templates
|
||||||
|
error.toolNotInstalled={0} is not installed
|
||||||
|
error.toolRequired={0} is required for {1}
|
||||||
|
error.conversionFailed={0} conversion failed
|
||||||
|
error.commandFailed={0} command failed
|
||||||
|
error.algorithmNotAvailable={0} algorithm not available
|
||||||
|
error.optionsNotSpecified={0} options are not specified
|
||||||
|
error.fileFormatRequired=File must be in {0} format
|
||||||
|
error.invalidFormat=Invalid {0} format: {1}
|
||||||
|
error.endpointDisabled=This endpoint has been disabled by the admin
|
||||||
|
error.urlNotReachable=URL is not reachable, please provide a valid URL
|
||||||
|
|
||||||
|
# DPI and image rendering messages - used by frontend for dynamic translation
|
||||||
|
# Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message
|
||||||
|
# Frontend parses this and replaces with localized versions using these keys
|
||||||
|
error.dpiExceedsLimit=DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause memory issues and crashes. Please use a lower DPI value.
|
||||||
|
error.pageTooBigForDpi=PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).
|
||||||
|
error.pageTooBigExceedsArray=PDF page {0} is too large to render at {1} DPI. The resulting image would exceed Java's maximum array size. Please try a lower DPI value (recommended: 150 or less).
|
||||||
|
error.pageTooBigFor300Dpi=PDF page {0} is too large to render at 300 DPI. The resulting image would exceed Java's maximum array size. Please use a lower DPI value for PDF-to-image conversion.
|
||||||
|
|
||||||
|
# URL and website conversion messages
|
||||||
|
|
||||||
|
# System requirements messages
|
||||||
|
|
||||||
|
# Authentication and security messages
|
||||||
|
error.apiKeyInvalid=API key is not valid.
|
||||||
|
error.userNotFound=User not found.
|
||||||
|
error.passwordRequired=Password must not be null.
|
||||||
|
error.accountLocked=Your account has been locked due to too many failed login attempts.
|
||||||
|
error.invalidEmail=Invalid email addresses provided.
|
||||||
|
error.emailAttachmentRequired=An attachment is required to send the email.
|
||||||
|
error.signatureNotFound=Signature file not found.
|
||||||
|
|
||||||
|
# File processing messages
|
||||||
|
error.fileNotFound=File not found with ID: {0}
|
||||||
|
|
||||||
|
# Database and configuration messages
|
||||||
|
error.noBackupScripts=No backup scripts were found.
|
||||||
|
error.unsupportedProvider={0} is not currently supported.
|
||||||
|
error.pathTraversalDetected=Path traversal detected for security reasons.
|
||||||
|
|
||||||
|
# Validation messages
|
||||||
|
error.invalidArgument=Invalid argument: {0}
|
||||||
|
error.argumentRequired={0} must not be null
|
||||||
|
error.operationFailed=Operation failed: {0}
|
||||||
|
error.angleNotMultipleOf90=Angle must be a multiple of 90
|
||||||
|
error.pdfBookmarksNotFound=No PDF bookmarks/outline found in document
|
||||||
|
error.fontLoadingFailed=Error processing font file
|
||||||
|
error.fontDirectoryReadFailed=Failed to read font directory
|
||||||
delete=Esborra
|
delete=Esborra
|
||||||
username=Usuari
|
username=Usuari
|
||||||
password=Contrasenya
|
password=Contrasenya
|
||||||
@@ -181,7 +242,6 @@ red=Vermell
|
|||||||
green=Verd
|
green=Verd
|
||||||
blue=Blau
|
blue=Blau
|
||||||
custom=Personalitzat...
|
custom=Personalitzat...
|
||||||
WorkInProgess=En desenvolupament, pot no funcionar o contenir errors. Si us plau, informa de qualsevol problema!
|
|
||||||
poweredBy=Impulsat per
|
poweredBy=Impulsat per
|
||||||
yes=Si
|
yes=Si
|
||||||
no=No
|
no=No
|
||||||
@@ -525,6 +585,10 @@ home.addImage.title=Afegir imatge a PDF
|
|||||||
home.addImage.desc=Afegeix una imatge en un PDF (en progrés)
|
home.addImage.desc=Afegeix una imatge en un PDF (en progrés)
|
||||||
addImage.tags=img,jpg,imatge,foto
|
addImage.tags=img,jpg,imatge,foto
|
||||||
|
|
||||||
|
home.attachments.title=Add Attachments
|
||||||
|
home.attachments.desc=Add or remove embedded files (attachments) to/from a PDF
|
||||||
|
attachments.tags=embed,attach,file,attachment,attachments
|
||||||
|
|
||||||
home.watermark.title=Afegir Marca d'aigua
|
home.watermark.title=Afegir Marca d'aigua
|
||||||
home.watermark.desc=Afegir una marca d'aigua personalitzada en un PDF
|
home.watermark.desc=Afegir una marca d'aigua personalitzada en un PDF
|
||||||
watermark.tags=text,repetició,etiqueta,propia,copyright,marca registrada,img,jpg,imatge,foto
|
watermark.tags=text,repetició,etiqueta,propia,copyright,marca registrada,img,jpg,imatge,foto
|
||||||
@@ -1205,6 +1269,12 @@ addImage.everyPage=Totes les pàgines?
|
|||||||
addImage.upload=Afegir Imatge
|
addImage.upload=Afegir Imatge
|
||||||
addImage.submit=Afegir Imatge
|
addImage.submit=Afegir Imatge
|
||||||
|
|
||||||
|
#attachments
|
||||||
|
attachments.title=Add Attachments
|
||||||
|
attachments.header=Add attachments
|
||||||
|
attachments.description=Allows you to add attachments to the PDF
|
||||||
|
attachments.descriptionPlaceholder=Enter a description for the attachments...
|
||||||
|
attachments.addButton=Add Attachments
|
||||||
|
|
||||||
#merge
|
#merge
|
||||||
merge.title=Fusiona
|
merge.title=Fusiona
|
||||||
@@ -1594,6 +1664,7 @@ fileChooser.dragAndDropPDF=Arrossega i deixa anar un fitxer PDF
|
|||||||
fileChooser.dragAndDropImage=Arrossega i deixa anar un fitxer d'imatge
|
fileChooser.dragAndDropImage=Arrossega i deixa anar un fitxer d'imatge
|
||||||
fileChooser.hoveredDragAndDrop=Arrossega i deixa anar fitxer(s) aquí
|
fileChooser.hoveredDragAndDrop=Arrossega i deixa anar fitxer(s) aquí
|
||||||
fileChooser.extractPDF=Extracting...
|
fileChooser.extractPDF=Extracting...
|
||||||
|
fileChooser.addAttachments=drag & drop attachments here
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
releases.footer=Llançaments
|
releases.footer=Llançaments
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user