mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-07-27 15:45:21 +00:00
Merge remote-tracking branch 'origin/main' into feature/react-overhaul
This commit is contained in:
commit
af8e065e87
2
.github/labeler-config-srvaroa.yml
vendored
2
.github/labeler-config-srvaroa.yml
vendored
@ -115,7 +115,7 @@ labels:
|
||||
- '.editorconfig'
|
||||
- '.pre-commit-config'
|
||||
- '.github/workflows/pre_commit.yml'
|
||||
- 'HowToAddNewLanguage.md'
|
||||
- 'devGuide/.*'
|
||||
|
||||
- label: 'Test'
|
||||
files:
|
||||
|
10
.github/pull_request_template.md
vendored
10
.github/pull_request_template.md
vendored
@ -1,5 +1,6 @@
|
||||
# Description of Changes
|
||||
|
||||
<!--
|
||||
Please provide a summary of the changes, including:
|
||||
|
||||
- What was changed
|
||||
@ -7,6 +8,7 @@ Please provide a summary of the changes, including:
|
||||
- Any challenges encountered
|
||||
|
||||
Closes #(issue_number)
|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
@ -15,15 +17,15 @@ Closes #(issue_number)
|
||||
### General
|
||||
|
||||
- [ ] 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 [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 [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/devGuide/HowToAddNewLanguage.md) (if applicable)
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] My changes generate no new warnings
|
||||
|
||||
### 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 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)
|
||||
|
||||
@ -31,4 +33,4 @@ Closes #(issue_number)
|
||||
|
||||
### 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.
|
||||
|
32
.github/scripts/requirements_pre_commit.txt
vendored
32
.github/scripts/requirements_pre_commit.txt
vendored
@ -2,7 +2,7 @@
|
||||
# This file is autogenerated by pip-compile with Python 3.10
|
||||
# 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 \
|
||||
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
|
||||
@ -12,25 +12,25 @@ distlib==0.3.9 \
|
||||
--hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \
|
||||
--hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403
|
||||
# via virtualenv
|
||||
filelock==3.16.1 \
|
||||
--hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \
|
||||
--hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435
|
||||
filelock==3.18.0 \
|
||||
--hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \
|
||||
--hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de
|
||||
# via virtualenv
|
||||
identify==2.6.5 \
|
||||
--hash=sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566 \
|
||||
--hash=sha256:c10b33f250e5bba374fae86fb57f3adcebf1161bce7cdf92031915fd480c13bc
|
||||
identify==2.6.12 \
|
||||
--hash=sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2 \
|
||||
--hash=sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6
|
||||
# via pre-commit
|
||||
nodeenv==1.9.1 \
|
||||
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
||||
# via pre-commit
|
||||
platformdirs==4.3.6 \
|
||||
--hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \
|
||||
--hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb
|
||||
platformdirs==4.3.8 \
|
||||
--hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \
|
||||
--hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4
|
||||
# via virtualenv
|
||||
pre-commit==4.0.1 \
|
||||
--hash=sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2 \
|
||||
--hash=sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878
|
||||
pre-commit==4.2.0 \
|
||||
--hash=sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146 \
|
||||
--hash=sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd
|
||||
# via -r .github\scripts\requirements_pre_commit.in
|
||||
pyyaml==6.0.2 \
|
||||
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
|
||||
@ -87,7 +87,7 @@ pyyaml==6.0.2 \
|
||||
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
|
||||
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
|
||||
# via pre-commit
|
||||
virtualenv==20.28.1 \
|
||||
--hash=sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb \
|
||||
--hash=sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329
|
||||
virtualenv==20.31.2 \
|
||||
--hash=sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11 \
|
||||
--hash=sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af
|
||||
# via pre-commit
|
||||
|
8
.github/scripts/requirements_sync_readme.txt
vendored
8
.github/scripts/requirements_sync_readme.txt
vendored
@ -2,9 +2,9 @@
|
||||
# This file is autogenerated by pip-compile with Python 3.10
|
||||
# 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 \
|
||||
--hash=sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde \
|
||||
--hash=sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79
|
||||
tomlkit==0.13.3 \
|
||||
--hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \
|
||||
--hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0
|
||||
# via -r .github\scripts\requirements_sync_readme.in
|
||||
|
@ -30,6 +30,7 @@ jobs:
|
||||
github.event.comment.user.login == 'sbplat' ||
|
||||
github.event.comment.user.login == 'reecebrowne' ||
|
||||
github.event.comment.user.login == 'DarioGii' ||
|
||||
github.event.comment.user.login == 'EthanHealy01' ||
|
||||
github.event.comment.user.login == 'ConnorYoh'
|
||||
)
|
||||
outputs:
|
||||
@ -42,7 +43,7 @@ jobs:
|
||||
enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -149,7 +150,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
2
.github/workflows/PR-Demo-cleanup.yml
vendored
2
.github/workflows/PR-Demo-cleanup.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
2
.github/workflows/ai_pr_title_review.yml
vendored
2
.github/workflows/ai_pr_title_review.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
2
.github/workflows/auto-labeler.yml
vendored
2
.github/workflows/auto-labeler.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
2
.github/workflows/auto-labelerV2.yml
vendored
2
.github/workflows/auto-labelerV2.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -91,7 +91,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -135,7 +135,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
2
.github/workflows/check_properties.yml
vendored
2
.github/workflows/check_properties.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
pull-requests: write # Allow writing to pull requests
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
2
.github/workflows/licenses-update.yml
vendored
2
.github/workflows/licenses-update.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
||||
repository-projects: write # Required for enabling automerge
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
2
.github/workflows/manage-label.yml
vendored
2
.github/workflows/manage-label.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
34
.github/workflows/multiOSReleases.yml
vendored
34
.github/workflows/multiOSReleases.yml
vendored
@ -21,27 +21,31 @@ jobs:
|
||||
versionMac: ${{ steps.versionNumberMac.outputs.versionNumberMac }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- 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
|
||||
id: versionNumber
|
||||
run: |
|
||||
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
||||
VERSION=$(./gradlew printVersion --quiet | tail -1)
|
||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
# ✅ Get Mac-specific version from Gradle
|
||||
- name: Get version number mac
|
||||
id: versionNumberMac
|
||||
run: |
|
||||
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
||||
CURRENT_YEAR=$(date +'%Y')
|
||||
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
|
||||
VERSION_MAC=$(./gradlew printMacVersion --quiet | tail -1)
|
||||
echo "versionNumberMac=$VERSION_MAC" >> $GITHUB_OUTPUT
|
||||
|
||||
build-portable:
|
||||
needs: read_versions
|
||||
@ -56,7 +60,7 @@ jobs:
|
||||
file_suffix: ""
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -82,7 +86,7 @@ jobs:
|
||||
run: |
|
||||
mkdir ./binaries
|
||||
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
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
@ -106,7 +110,7 @@ jobs:
|
||||
file_suffix: ""
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -144,7 +148,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -234,7 +238,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -248,7 +252,7 @@ jobs:
|
||||
|
||||
- name: Install Cosign
|
||||
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
|
||||
if: matrix.os == 'windows-latest'
|
||||
@ -297,7 +301,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
2
.github/workflows/pre_commit.yml
vendored
2
.github/workflows/pre_commit.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
10
.github/workflows/push-docker.yml
vendored
10
.github/workflows/push-docker.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
- name: Install cosign
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: sigstore/cosign-installer@fb28c2b6339dcd94da6e4cbcbc5e888961f6f8c3 # v3.9.0
|
||||
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1
|
||||
with:
|
||||
cosign-release: "v2.4.1"
|
||||
|
||||
@ -77,6 +77,7 @@ jobs:
|
||||
- name: Generate tags
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
if: github.ref != 'refs/heads/main'
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
@ -86,11 +87,11 @@ jobs:
|
||||
tags: |
|
||||
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=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Build and push main Dockerfile
|
||||
id: build-push-regular
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
if: github.ref != 'refs/heads/main'
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
@ -153,7 +154,6 @@ jobs:
|
||||
- name: Generate tags fat
|
||||
id: meta3
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
if: github.ref != 'refs/heads/main'
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
@ -163,11 +163,11 @@ jobs:
|
||||
tags: |
|
||||
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=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Build and push main Dockerfile fat
|
||||
id: build-push-fat
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
if: github.ref != 'refs/heads/main'
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
|
8
.github/workflows/releaseArtifacts.yml
vendored
8
.github/workflows/releaseArtifacts.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -83,7 +83,7 @@ jobs:
|
||||
file_suffix: ""
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -95,7 +95,7 @@ jobs:
|
||||
run: ls -R
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@fb28c2b6339dcd94da6e4cbcbc5e888961f6f8c3 # v3.9.0
|
||||
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1
|
||||
|
||||
- name: Generate key pair
|
||||
run: cosign generate-key-pair
|
||||
@ -161,7 +161,7 @@ jobs:
|
||||
file_suffix: ""
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
4
.github/workflows/scorecards.yml
vendored
4
.github/workflows/scorecards.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -74,6 +74,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- 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:
|
||||
sarif_file: results.sarif
|
||||
|
2
.github/workflows/sonarqube.yml
vendored
2
.github/workflows/sonarqube.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
2
.github/workflows/swagger.yml
vendored
2
.github/workflows/swagger.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
6
.github/workflows/sync_files.yml
vendored
6
.github/workflows/sync_files.yml
vendored
@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -57,8 +57,8 @@ jobs:
|
||||
|
||||
- name: Run git add
|
||||
run: |
|
||||
git add README.md
|
||||
git diff --staged --quiet || git commit -m ":memo: Sync README.md" || echo "No changes detected"
|
||||
git add README.md scripts/ignore_translation.toml
|
||||
git diff --staged --quiet || git commit -m ":memo: Sync README.md & scripts/ignore_translation.toml" || echo "No changes detected"
|
||||
|
||||
- name: Create Pull Request
|
||||
if: always()
|
||||
|
6
.github/workflows/testdriver.yml
vendored
6
.github/workflows/testdriver.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -105,7 +105,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@ -134,7 +134,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
@ -25,7 +25,7 @@ Please make sure your Pull Request adheres to the following guidelines:
|
||||
|
||||
## 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
|
||||
|
||||
@ -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).
|
||||
|
||||
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
|
||||
|
||||
|
103
Dockerfile
Normal file
103
Dockerfile
Normal file
@ -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"]
|
61
Dockerfile.dev
Normal file
61
Dockerfile.dev
Normal file
@ -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
Dockerfile.fat
Normal file
114
Dockerfile.fat
Normal file
@ -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"]
|
55
Dockerfile.ultra-lite
Normal file
55
Dockerfile.ultra-lite
Normal file
@ -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"]
|
80
README.md
80
README.md
@ -29,7 +29,7 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir
|
||||
- API for integration with external scripts
|
||||
- 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)
|
||||
- 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
|
||||
|
||||
@ -116,47 +116,47 @@ Stirling-PDF currently supports 40 languages!
|
||||
|
||||
| Language | Progress |
|
||||
| -------------------------------------------- | -------------------------------------- |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| English (English) (en_GB) |  |
|
||||
| English (US) (en_US) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Tibetan (བོད་ཡིག་) (bo_CN) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Malayalam (മലയാളം) (ml_IN) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Tibetan (བོད་ཡིག་) (bo_CN) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Malayalam (മലയാളം) (ml_IN) |  |
|
||||
|
||||
## Stirling PDF Enterprise
|
||||
|
||||
@ -168,7 +168,7 @@ Check out our [Enterprise docs](https://docs.stirlingpdf.com/Pro)
|
||||
|
||||
Join our community:
|
||||
- [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)
|
||||
- [Discord Community](https://discord.gg/HYmhKj45pU)
|
||||
- [Developer Guide](DeveloperGuide.md)
|
||||
|
96
build.gradle
96
build.gradle
@ -9,12 +9,14 @@ plugins {
|
||||
id "com.diffplug.spotless" version "7.0.4"
|
||||
id "com.github.jk1.dependency-license-report" version "2.9"
|
||||
//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"
|
||||
}
|
||||
|
||||
import com.github.jk1.license.render.*
|
||||
import org.gradle.internal.os.OperatingSystem
|
||||
import org.panteleyev.jpackage.ImageType
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.time.Year
|
||||
|
||||
@ -26,7 +28,7 @@ ext {
|
||||
bouncycastleVersion = "1.81"
|
||||
springSecuritySamlVersion = "6.5.1"
|
||||
openSamlVersion = "4.3.2"
|
||||
commonmarkVersion = "0.24.0"
|
||||
commonmarkVersion = "0.25.0"
|
||||
googleJavaFormatVersion = "1.27.0"
|
||||
tempJrePath = null
|
||||
}
|
||||
@ -43,39 +45,19 @@ bootJar {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
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/**'
|
||||
}
|
||||
// Configure main class for the root project
|
||||
springBoot {
|
||||
mainClass = 'stirling.software.SPDF.SPDFApplication'
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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/**'
|
||||
}
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url = 'https://build.shibboleth.net/maven/releases' }
|
||||
}
|
||||
|
||||
allprojects {
|
||||
group = 'stirling.software'
|
||||
version = '1.0.0'
|
||||
version = '1.0.2'
|
||||
|
||||
configurations.configureEach {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
@ -83,7 +65,6 @@ allprojects {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
tasks.register('writeVersion') {
|
||||
def propsFile = file("$projectDir/common/src/main/resources/version.properties")
|
||||
def propsDir = propsFile.parentFile
|
||||
@ -108,6 +89,10 @@ tasks.register('writeVersion') {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('createExe') {
|
||||
dependsOn(":stirling-pdf:bootJar")
|
||||
}
|
||||
|
||||
subprojects {
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'java-library'
|
||||
@ -120,9 +105,11 @@ subprojects {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
bootJar {
|
||||
enabled = false
|
||||
}
|
||||
if (project.name != "stirling-pdf") {
|
||||
bootJar {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@ -170,7 +157,7 @@ subprojects {
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
|
||||
tasks.named("processResources") {
|
||||
dependsOn(rootProject.tasks.writeVersion)
|
||||
}
|
||||
@ -224,6 +211,14 @@ openApi {
|
||||
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
|
||||
static def getMacVersion(String version) {
|
||||
def currentYear = Year.now().getValue()
|
||||
@ -232,14 +227,15 @@ static def getMacVersion(String version) {
|
||||
}
|
||||
|
||||
jpackage {
|
||||
input = "build/libs"
|
||||
destination = "${projectDir}/build/jpackage"
|
||||
dependsOn(":stirling-pdf:bootJar")
|
||||
input = layout.projectDirectory.dir("stirling-pdf/build/libs")
|
||||
destination = layout.projectDirectory.dir("build/jpackage")
|
||||
mainJar = "Stirling-PDF-${project.version}.jar"
|
||||
appName = "Stirling PDF"
|
||||
appVersion = project.version
|
||||
vendor = "Stirling PDF Inc"
|
||||
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
|
||||
// mainClass = "org.springframework.boot.loader.launch.JarLauncher"
|
||||
|
||||
@ -274,15 +270,15 @@ jpackage {
|
||||
winUpgradeUuid = "2a43ed0c-b8c2-40cf-89e1-751129b87641" // Unique identifier for updates
|
||||
winHelpUrl = "https://github.com/Stirling-Tools/Stirling-PDF"
|
||||
winUpdateUrl = "https://github.com/Stirling-Tools/Stirling-PDF/releases"
|
||||
type = "exe"
|
||||
type = ImageType.EXE
|
||||
installDir = "C:/Program Files/Stirling-PDF"
|
||||
}
|
||||
|
||||
// MacOS-specific configuration
|
||||
mac {
|
||||
appVersion = getMacVersion(project.version.toString())
|
||||
icon = "stirling-pdf/src/main/resources/static/favicon.icns"
|
||||
type = "dmg"
|
||||
icon = layout.projectDirectory.file("stirling-pdf/src/main/resources/static/favicon.icns")
|
||||
type = ImageType.DMG
|
||||
macPackageIdentifier = "Stirling PDF"
|
||||
macPackageName = "Stirling PDF"
|
||||
macAppCategory = "public.app-category.productivity"
|
||||
@ -303,8 +299,8 @@ jpackage {
|
||||
// Linux-specific configuration
|
||||
linux {
|
||||
appVersion = project.version
|
||||
icon = "stirling-pdf/src/main/resources/static/favicon.png"
|
||||
type = "deb" // Can also use "rpm" for Red Hat-based systems
|
||||
icon = layout.projectDirectory.file("stirling-pdf/src/main/resources/static/favicon.png")
|
||||
type = ImageType.DEB // Can also use "rpm" for Red Hat-based systems
|
||||
|
||||
// Debian package configuration
|
||||
//linuxPackageName = "stirlingpdf"
|
||||
@ -340,7 +336,7 @@ jpackage {
|
||||
|
||||
// Add copyright and license information
|
||||
copyright = "Copyright © 2025 Stirling PDF Inc."
|
||||
licenseFile = "LICENSE"
|
||||
licenseFile = layout.projectDirectory.file("LICENSE")
|
||||
}
|
||||
|
||||
//tasks.wrapper {
|
||||
@ -375,7 +371,7 @@ tasks.register('jpackageMacX64') {
|
||||
commandLine 'jpackage',
|
||||
'--type', 'dmg',
|
||||
'--name', 'Stirling PDF (x86_64)',
|
||||
'--input', 'build/libs',
|
||||
'--input', 'stirling-pdf/build/libs',
|
||||
'--main-jar', "Stirling-PDF-${project.version}.jar",
|
||||
'--main-class', 'org.springframework.boot.loader.launch.JarLauncher',
|
||||
'--runtime-image', file(jrePath + "/zulu-17.jre/Contents/Home"),
|
||||
@ -475,7 +471,7 @@ launch4j {
|
||||
} else {
|
||||
headerType = "console"
|
||||
}
|
||||
jarTask = tasks.bootJar
|
||||
jarTask = project(":stirling-pdf").tasks.bootJar
|
||||
|
||||
errTitle="Encountered error, do you have Java 21?"
|
||||
downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe"
|
||||
@ -537,6 +533,14 @@ swaggerhubUpload {
|
||||
}
|
||||
|
||||
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'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.2'
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import java.util.Properties;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
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.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||
|
||||
import lombok.Getter;
|
||||
@ -148,23 +148,10 @@ public class AppConfig {
|
||||
}
|
||||
|
||||
@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() {
|
||||
return true;
|
||||
return ClassUtils.isPresent(
|
||||
"stirling.software.proprietary.security.configuration.SecurityConfiguration",
|
||||
this.getClass().getClassLoader());
|
||||
}
|
||||
|
||||
@Bean(name = "directoryFilter")
|
||||
|
@ -545,6 +545,8 @@ public class ApplicationProperties {
|
||||
private int calibreSessionLimit;
|
||||
private int qpdfSessionLimit;
|
||||
private int tesseractSessionLimit;
|
||||
private int ghostscriptSessionLimit;
|
||||
private int ocrMyPdfSessionLimit;
|
||||
|
||||
public int getQpdfSessionLimit() {
|
||||
return qpdfSessionLimit > 0 ? qpdfSessionLimit : 2;
|
||||
@ -577,6 +579,14 @@ public class ApplicationProperties {
|
||||
public int getCalibreSessionLimit() {
|
||||
return calibreSessionLimit > 0 ? calibreSessionLimit : 1;
|
||||
}
|
||||
|
||||
public int getGhostscriptSessionLimit() {
|
||||
return ghostscriptSessionLimit > 0 ? ghostscriptSessionLimit : 8;
|
||||
}
|
||||
|
||||
public int getOcrMyPdfSessionLimit() {
|
||||
return ocrMyPdfSessionLimit > 0 ? ocrMyPdfSessionLimit : 2;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@ -589,6 +599,8 @@ public class ApplicationProperties {
|
||||
private long calibreTimeoutMinutes;
|
||||
private long tesseractTimeoutMinutes;
|
||||
private long qpdfTimeoutMinutes;
|
||||
private long ghostscriptTimeoutMinutes;
|
||||
private long ocrMyPdfTimeoutMinutes;
|
||||
|
||||
public long getTesseractTimeoutMinutes() {
|
||||
return tesseractTimeoutMinutes > 0 ? tesseractTimeoutMinutes : 30;
|
||||
@ -621,6 +633,14 @@ public class ApplicationProperties {
|
||||
public long getCalibreTimeoutMinutes() {
|
||||
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;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
@ -26,14 +27,8 @@ public class JobResult {
|
||||
/** Error message if the job failed */
|
||||
private String error;
|
||||
|
||||
/** The file ID of the result file, if applicable */
|
||||
private String fileId;
|
||||
|
||||
/** Original file name, if applicable */
|
||||
private String originalFileName;
|
||||
|
||||
/** MIME type of the result, if applicable */
|
||||
private String contentType;
|
||||
/** List of result files for jobs that produce files */
|
||||
private List<ResultFile> resultFiles;
|
||||
|
||||
/** Time when the job was created */
|
||||
private LocalDateTime createdAt;
|
||||
@ -64,21 +59,6 @@ public class JobResult {
|
||||
.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
|
||||
*
|
||||
@ -101,6 +81,67 @@ public class JobResult {
|
||||
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
|
||||
*
|
||||
|
@ -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;
|
||||
}
|
@ -24,6 +24,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.util.ApplicationContextProvider;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.TempFileRegistry;
|
||||
|
||||
@ -82,7 +83,7 @@ public class CustomPDFDocumentFactory {
|
||||
*/
|
||||
public PDDocument load(File file, boolean readOnly) throws IOException {
|
||||
if (file == null) {
|
||||
throw new IllegalArgumentException("File cannot be null");
|
||||
throw ExceptionUtils.createNullArgumentException("File");
|
||||
}
|
||||
|
||||
long fileSize = file.length();
|
||||
@ -109,7 +110,7 @@ public class CustomPDFDocumentFactory {
|
||||
*/
|
||||
public PDDocument load(Path path, boolean readOnly) throws IOException {
|
||||
if (path == null) {
|
||||
throw new IllegalArgumentException("File cannot be null");
|
||||
throw ExceptionUtils.createNullArgumentException("File");
|
||||
}
|
||||
|
||||
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. */
|
||||
public PDDocument load(byte[] input, boolean readOnly) throws IOException {
|
||||
if (input == null) {
|
||||
throw new IllegalArgumentException("Input bytes cannot be null");
|
||||
throw ExceptionUtils.createNullArgumentException("Input bytes");
|
||||
}
|
||||
|
||||
long dataSize = input.length;
|
||||
@ -151,7 +152,7 @@ public class CustomPDFDocumentFactory {
|
||||
/** Load a PDF from InputStream with automatic optimization and read-only option. */
|
||||
public PDDocument load(InputStream input, boolean readOnly) throws IOException {
|
||||
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
|
||||
@ -174,7 +175,7 @@ public class CustomPDFDocumentFactory {
|
||||
public PDDocument load(InputStream input, String password, boolean readOnly)
|
||||
throws IOException {
|
||||
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
|
||||
@ -292,9 +293,32 @@ public class CustomPDFDocumentFactory {
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported source type: " + source.getClass());
|
||||
}
|
||||
|
||||
configureResourceCacheIfNeeded(document, contentSize);
|
||||
|
||||
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 */
|
||||
private PDDocument loadAdaptivelyWithPassword(Object source, long contentSize, String password)
|
||||
throws IOException {
|
||||
@ -313,6 +337,9 @@ public class CustomPDFDocumentFactory {
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported source type: " + source.getClass());
|
||||
}
|
||||
|
||||
configureResourceCacheIfNeeded(document, contentSize);
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
@ -354,7 +381,12 @@ public class CustomPDFDocumentFactory {
|
||||
|
||||
private PDDocument loadFromFile(File file, long size, StreamCacheCreateFunction cache)
|
||||
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)
|
||||
@ -366,7 +398,13 @@ public class CustomPDFDocumentFactory {
|
||||
Files.write(tempFile, bytes);
|
||||
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 {
|
||||
@ -399,7 +437,7 @@ public class CustomPDFDocumentFactory {
|
||||
try {
|
||||
document.setAllSecurityToBeRemoved(true);
|
||||
} catch (Exception e) {
|
||||
log.error("Decryption failed", e);
|
||||
ExceptionUtils.logException("PDF decryption", e);
|
||||
throw new IOException("PDF decryption failed", e);
|
||||
}
|
||||
}
|
||||
|
@ -131,14 +131,46 @@ public class FileStorage {
|
||||
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
|
||||
*
|
||||
* @param fileId The ID of 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) {
|
||||
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;
|
||||
|
||||
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.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
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.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
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.JobStats;
|
||||
import stirling.software.common.model.job.ResultFile;
|
||||
|
||||
/** Manages async tasks and their results */
|
||||
@Service
|
||||
@ -80,8 +91,53 @@ public class TaskManager {
|
||||
public void setFileResult(
|
||||
String jobId, String fileId, String originalFileName, String contentType) {
|
||||
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) {
|
||||
JobResult jobResult = getOrCreateJobResult(jobId);
|
||||
if (jobResult.getResult() == null
|
||||
&& jobResult.getFileId() == null
|
||||
&& !jobResult.hasFiles()
|
||||
&& jobResult.getError() == null) {
|
||||
// If no result or error has been set, mark it as complete with an empty result
|
||||
jobResult.completeWithResult("Task completed successfully");
|
||||
@ -186,7 +242,7 @@ public class TaskManager {
|
||||
failedJobs++;
|
||||
} else {
|
||||
successfulJobs++;
|
||||
if (result.getFileId() != null) {
|
||||
if (result.hasFiles()) {
|
||||
fileResultJobs++;
|
||||
}
|
||||
}
|
||||
@ -250,17 +306,8 @@ public class TaskManager {
|
||||
&& result.getCompletedAt() != null
|
||||
&& result.getCompletedAt().isBefore(expiryThreshold)) {
|
||||
|
||||
// If the job has a file result, delete the file
|
||||
if (result.getFileId() != null) {
|
||||
try {
|
||||
fileStorage.deleteFile(result.getFileId());
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to delete file for job {}: {}",
|
||||
entry.getKey(),
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
// Clean up file results
|
||||
cleanupJobFiles(result, entry.getKey());
|
||||
|
||||
// Remove the job result
|
||||
jobResults.remove(entry.getKey());
|
||||
@ -290,4 +337,128 @@ public class TaskManager {
|
||||
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();
|
||||
int unregisteredDeletedCount = cleanupUnregisteredFiles(containerMode, true, maxAgeMillis);
|
||||
|
||||
log.info(
|
||||
"Scheduled cleanup complete. Deleted {} registered files, {} unregistered files, {} directories",
|
||||
registeredDeletedCount,
|
||||
unregisteredDeletedCount,
|
||||
directoriesDeletedCount);
|
||||
if (registeredDeletedCount > 0
|
||||
|| unregisteredDeletedCount > 0
|
||||
|| directoriesDeletedCount > 0) {
|
||||
log.info(
|
||||
"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.
|
||||
*/
|
||||
private void runStartupCleanup() {
|
||||
log.info("Running startup temporary file cleanup");
|
||||
boolean containerMode = isContainerMode();
|
||||
|
||||
log.info(
|
||||
@ -178,7 +181,6 @@ public class TempFileCleanupService {
|
||||
long maxAgeMillis = containerMode ? 0 : 24 * 60 * 60 * 1000; // 0 or 24 hours
|
||||
|
||||
int totalDeletedCount = cleanupUnregisteredFiles(containerMode, false, maxAgeMillis);
|
||||
|
||||
log.info(
|
||||
"Startup cleanup complete. Deleted {} temporary files/directories",
|
||||
totalDeletedCount);
|
||||
@ -225,7 +227,7 @@ public class TempFileCleanupService {
|
||||
tempDir -> {
|
||||
try {
|
||||
String phase = isScheduled ? "scheduled" : "startup";
|
||||
log.info(
|
||||
log.debug(
|
||||
"Scanning directory for {} cleanup: {}",
|
||||
phase,
|
||||
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 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 =
|
||||
sanitizeHtmlContent(
|
||||
new String(fileBytes, StandardCharsets.UTF_8), disableSanitize);
|
||||
Files.write(
|
||||
tempInputFile.getPath(),
|
||||
sanitizedHtml.getBytes(StandardCharsets.UTF_8));
|
||||
} else if (fileName.endsWith(".zip")) {
|
||||
} else if (fileName.toLowerCase().endsWith(".zip")) {
|
||||
Files.write(tempInputFile.getPath(), fileBytes);
|
||||
sanitizeHtmlFilesInZip(
|
||||
tempInputFile.getPath(), disableSanitize, tempFileManager);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported file format: " + fileName);
|
||||
throw ExceptionUtils.createHtmlFileRequiredException();
|
||||
}
|
||||
|
||||
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) {
|
||||
switch (size.toUpperCase()) {
|
||||
case "A0":
|
||||
case "A0" -> {
|
||||
return PDRectangle.A0;
|
||||
case "A1":
|
||||
}
|
||||
case "A1" -> {
|
||||
return PDRectangle.A1;
|
||||
case "A2":
|
||||
}
|
||||
case "A2" -> {
|
||||
return PDRectangle.A2;
|
||||
case "A3":
|
||||
}
|
||||
case "A3" -> {
|
||||
return PDRectangle.A3;
|
||||
case "A4":
|
||||
}
|
||||
case "A4" -> {
|
||||
return PDRectangle.A4;
|
||||
case "A5":
|
||||
}
|
||||
case "A5" -> {
|
||||
return PDRectangle.A5;
|
||||
case "A6":
|
||||
}
|
||||
case "A6" -> {
|
||||
return PDRectangle.A6;
|
||||
case "LETTER":
|
||||
}
|
||||
case "LETTER" -> {
|
||||
return PDRectangle.LETTER;
|
||||
case "LEGAL":
|
||||
}
|
||||
case "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,
|
||||
String filename)
|
||||
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)) {
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
pdfRenderer.setSubsamplingAllowed(true);
|
||||
@ -158,7 +177,21 @@ public class PdfUtils {
|
||||
writer.prepareWriteSequence(null);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -190,7 +223,20 @@ public class PdfUtils {
|
||||
PdfImageDimensionValue dimension = pageSizes.get(settings);
|
||||
if (dimension == null) {
|
||||
// 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;
|
||||
dimension =
|
||||
new PdfImageDimensionValue(
|
||||
@ -218,7 +264,20 @@ public class PdfUtils {
|
||||
if (firstImageAlreadyRendered && i == 0) {
|
||||
pageImage = pdfSizeImage;
|
||||
} 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
|
||||
@ -238,7 +297,20 @@ public class PdfUtils {
|
||||
// Zip the images and return as byte array
|
||||
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
|
||||
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()) {
|
||||
ImageIO.write(image, imageType, baosImage);
|
||||
|
||||
@ -276,7 +348,19 @@ public class PdfUtils {
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
pdfRenderer.setSubsamplingAllowed(true);
|
||||
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);
|
||||
|
||||
float width = originalPage.getMediaBox().getWidth();
|
||||
@ -349,7 +433,7 @@ public class PdfUtils {
|
||||
}
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
doc.save(byteArrayOutputStream);
|
||||
log.info("PDF successfully saved to byte array");
|
||||
log.debug("PDF successfully saved to byte array");
|
||||
return byteArrayOutputStream.toByteArray();
|
||||
}
|
||||
}
|
||||
@ -495,8 +579,7 @@ public class PdfUtils {
|
||||
case "less":
|
||||
return actualPageCount < pageCount;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid comparator. Only 'greater', 'equal', and 'less' are supported.");
|
||||
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,6 +84,16 @@ public class ProcessExecutor {
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getCalibreSessionLimit();
|
||||
case GHOSTSCRIPT ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getGhostscriptSessionLimit();
|
||||
case OCR_MY_PDF ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getOcrMyPdfSessionLimit();
|
||||
};
|
||||
|
||||
long timeoutMinutes =
|
||||
@ -128,6 +138,16 @@ public class ProcessExecutor {
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getCalibreTimeoutMinutes();
|
||||
case GHOSTSCRIPT ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getGhostscriptTimeoutMinutes();
|
||||
case OCR_MY_PDF ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getOcrMyPdfTimeoutMinutes();
|
||||
};
|
||||
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
|
||||
});
|
||||
@ -278,7 +298,9 @@ public class ProcessExecutor {
|
||||
INSTALL_APP,
|
||||
CALIBRE,
|
||||
TESSERACT,
|
||||
QPDF
|
||||
QPDF,
|
||||
GHOSTSCRIPT,
|
||||
OCR_MY_PDF
|
||||
}
|
||||
|
||||
public class ProcessExecutorResult {
|
||||
|
@ -108,6 +108,12 @@ public class CustomColorReplaceStrategy extends ReplaceAndInvertColorStrategy {
|
||||
} catch (IllegalArgumentException ie) {
|
||||
log.info("text not supported by font ");
|
||||
font = checkSupportedFontForCharacter(unicodeText);
|
||||
} catch (UnsupportedOperationException ue) {
|
||||
log.info(
|
||||
"font does not support encoding operation: {} for text: '{}'",
|
||||
font.getClass().getSimpleName(),
|
||||
unicodeText);
|
||||
font = checkSupportedFontForCharacter(unicodeText);
|
||||
} finally {
|
||||
// if any other font is not supported, then replace default character *
|
||||
if (font == null) {
|
||||
|
@ -126,7 +126,7 @@ class AutoJobPostMappingIntegrationTest {
|
||||
|
||||
verify(jobExecutorService).runJobGeneric(
|
||||
asyncCaptor.capture(),
|
||||
workCaptor.capture(),
|
||||
workCaptor.capture(),
|
||||
timeoutCaptor.capture(),
|
||||
queueableCaptor.capture(),
|
||||
resourceWeightCaptor.capture());
|
||||
@ -197,7 +197,7 @@ class AutoJobPostMappingIntegrationTest {
|
||||
autoJobAspect.wrapWithJobExecution(joinPoint, autoJobPostMapping);
|
||||
|
||||
// Then
|
||||
assertEquals("stored-file-id", pdfFile.getFileId(),
|
||||
assertEquals("stored-file-id", pdfFile.getFileId(),
|
||||
"FileId should be set to the stored file id");
|
||||
assertNotNull(pdfFile.getFileInput(), "FileInput should be replaced with persistent file");
|
||||
|
||||
|
@ -187,4 +187,4 @@ class FileStorageTest {
|
||||
// Assert
|
||||
assertFalse(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,8 +64,8 @@ class JobExecutorServiceTest {
|
||||
void setUp() {
|
||||
// Initialize the service manually with all its dependencies
|
||||
jobExecutorService = new JobExecutorService(
|
||||
taskManager,
|
||||
fileStorage,
|
||||
taskManager,
|
||||
fileStorage,
|
||||
request,
|
||||
resourceMonitor,
|
||||
jobQueue,
|
||||
@ -199,4 +199,4 @@ class JobExecutorServiceTest {
|
||||
assertTrue(e.getCause() instanceof TimeoutException);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ class JobQueueTest {
|
||||
// Mark stubbing as lenient to avoid UnnecessaryStubbingException
|
||||
lenient().when(resourceMonitor.calculateDynamicQueueCapacity(anyInt(), anyInt())).thenReturn(10);
|
||||
lenient().when(resourceMonitor.getCurrentStatus()).thenReturn(statusRef);
|
||||
|
||||
|
||||
// Initialize JobQueue with mocked ResourceMonitor
|
||||
jobQueue = new JobQueue(resourceMonitor);
|
||||
}
|
||||
@ -99,4 +99,4 @@ class JobQueueTest {
|
||||
assertTrue(jobQueue.isJobQueued(jobId));
|
||||
assertFalse(jobQueue.isJobQueued("nonexistent"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ class ResourceMonitorTest {
|
||||
|
||||
// Then
|
||||
assertEquals(shouldQueue, result,
|
||||
String.format("For weight %d and status %s, shouldQueue should be %s",
|
||||
String.format("For weight %d and status %s, shouldQueue should be %s",
|
||||
weight, status, shouldQueue));
|
||||
}
|
||||
|
||||
@ -134,4 +134,4 @@ class ResourceMonitorTest {
|
||||
assertTrue(staleMetrics.isStale(5000), "Metrics from 6 seconds ago should be stale with 5s threshold");
|
||||
assertFalse(freshMetrics.isStale(5000), "Fresh metrics should not be stale");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import stirling.software.common.model.job.JobResult;
|
||||
import stirling.software.common.model.job.JobStats;
|
||||
import stirling.software.common.model.job.ResultFile;
|
||||
|
||||
class TaskManagerTest {
|
||||
|
||||
@ -73,13 +74,17 @@ class TaskManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetFileResult() {
|
||||
void testSetFileResult() throws Exception {
|
||||
// Arrange
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
taskManager.createTask(jobId);
|
||||
String fileId = "file-id";
|
||||
String originalFileName = "test.pdf";
|
||||
String contentType = "application/pdf";
|
||||
long fileSize = 1024L;
|
||||
|
||||
// Mock the fileStorage.getFileSize() call
|
||||
when(fileStorage.getFileSize(fileId)).thenReturn(fileSize);
|
||||
|
||||
// Act
|
||||
taskManager.setFileResult(jobId, fileId, originalFileName, contentType);
|
||||
@ -88,9 +93,17 @@ class TaskManagerTest {
|
||||
JobResult result = taskManager.getJobResult(jobId);
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isComplete());
|
||||
assertEquals(fileId, result.getFileId());
|
||||
assertEquals(originalFileName, result.getOriginalFileName());
|
||||
assertEquals(contentType, result.getContentType());
|
||||
assertTrue(result.hasFiles());
|
||||
assertFalse(result.hasMultipleFiles());
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
@ -163,8 +176,11 @@ class TaskManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetJobStats() {
|
||||
void testGetJobStats() throws Exception {
|
||||
// Arrange
|
||||
// Mock fileStorage.getFileSize for file operations
|
||||
when(fileStorage.getFileSize("file-id")).thenReturn(1024L);
|
||||
|
||||
// 1. Create active job
|
||||
String activeJobId = "active-job";
|
||||
taskManager.createTask(activeJobId);
|
||||
@ -216,9 +232,15 @@ class TaskManagerTest {
|
||||
LocalDateTime oldTime = LocalDateTime.now().minusHours(1);
|
||||
ReflectionTestUtils.setField(oldJob, "completedAt", oldTime);
|
||||
ReflectionTestUtils.setField(oldJob, "complete", true);
|
||||
ReflectionTestUtils.setField(oldJob, "fileId", "file-id");
|
||||
ReflectionTestUtils.setField(oldJob, "originalFileName", "test.pdf");
|
||||
ReflectionTestUtils.setField(oldJob, "contentType", "application/pdf");
|
||||
|
||||
// Create a ResultFile and set it using the new approach
|
||||
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);
|
||||
|
||||
@ -252,17 +274,17 @@ class TaskManagerTest {
|
||||
// Verify the executor service is shutdown
|
||||
// This is difficult to test directly, but we can verify it doesn't throw exceptions
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testAddNote() {
|
||||
// Arrange
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
taskManager.createTask(jobId);
|
||||
String note = "Test note";
|
||||
|
||||
|
||||
// Act
|
||||
boolean result = taskManager.addNote(jobId, note);
|
||||
|
||||
|
||||
// Assert
|
||||
assertTrue(result);
|
||||
JobResult jobResult = taskManager.getJobResult(jobId);
|
||||
@ -271,16 +293,16 @@ class TaskManagerTest {
|
||||
assertEquals(1, jobResult.getNotes().size());
|
||||
assertEquals(note, jobResult.getNotes().get(0));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testAddNote_NonExistentJob() {
|
||||
// Arrange
|
||||
String jobId = "non-existent-job";
|
||||
String note = "Test note";
|
||||
|
||||
|
||||
// Act
|
||||
boolean result = taskManager.addNote(jobId, note);
|
||||
|
||||
|
||||
// Assert
|
||||
assertFalse(result);
|
||||
}
|
||||
|
@ -82,10 +82,10 @@ public class TempFileCleanupServiceTest {
|
||||
when(tempFileManagement.isStartupCleanup()).thenReturn(false);
|
||||
when(tempFileManagement.isCleanupSystemTemp()).thenReturn(false);
|
||||
when(tempFileManagement.getCleanupIntervalMinutes()).thenReturn(30L);
|
||||
|
||||
|
||||
// Set machineType using reflection (still needed for this field)
|
||||
ReflectionTestUtils.setField(cleanupService, "machineType", "Standard");
|
||||
|
||||
|
||||
when(tempFileManager.getMaxAgeMillis()).thenReturn(3600000L); // 1 hour
|
||||
}
|
||||
|
||||
@ -113,30 +113,30 @@ public class TempFileCleanupServiceTest {
|
||||
Path ourTempFile3 = Files.createFile(customTempDir.resolve("stirling-pdf-789.tmp"));
|
||||
Path ourTempFile4 = Files.createFile(customTempDir.resolve("pdf-save-123-456.tmp"));
|
||||
Path ourTempFile5 = Files.createFile(libreOfficeTempDir.resolve("input_file.pdf"));
|
||||
|
||||
|
||||
// Old temporary files
|
||||
Path oldTempFile = Files.createFile(systemTempDir.resolve("output_old.pdf"));
|
||||
|
||||
|
||||
// System temp files that should be cleaned in container mode
|
||||
Path sysTempFile1 = Files.createFile(systemTempDir.resolve("lu123abc.tmp"));
|
||||
Path sysTempFile2 = Files.createFile(customTempDir.resolve("ocr_process123"));
|
||||
Path sysTempFile3 = Files.createFile(customTempDir.resolve("tmp_upload.tmp"));
|
||||
|
||||
|
||||
// Files that should be preserved
|
||||
Path jettyFile1 = Files.createFile(systemTempDir.resolve("jetty-123.tmp"));
|
||||
Path jettyFile2 = Files.createFile(systemTempDir.resolve("something-with-jetty-inside.tmp"));
|
||||
Path regularFile = Files.createFile(systemTempDir.resolve("important.txt"));
|
||||
|
||||
|
||||
// Create a nested directory with temp files
|
||||
Path nestedDir = Files.createDirectories(systemTempDir.resolve("nested"));
|
||||
Path nestedTempFile = Files.createFile(nestedDir.resolve("output_nested.pdf"));
|
||||
|
||||
|
||||
// Empty file (special case)
|
||||
Path emptyFile = Files.createFile(systemTempDir.resolve("empty.tmp"));
|
||||
|
||||
|
||||
// Configure mock registry to say these files aren't registered
|
||||
when(registry.contains(any(File.class))).thenReturn(false);
|
||||
|
||||
|
||||
// The set of files that will be deleted in our test
|
||||
Set<Path> deletedFiles = new HashSet<>();
|
||||
|
||||
@ -145,31 +145,31 @@ public class TempFileCleanupServiceTest {
|
||||
// Mock Files.list for each directory we'll process
|
||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
||||
.thenReturn(Stream.of(
|
||||
ourTempFile1, ourTempFile2, oldTempFile, sysTempFile1,
|
||||
ourTempFile1, ourTempFile2, oldTempFile, sysTempFile1,
|
||||
jettyFile1, jettyFile2, regularFile, emptyFile, nestedDir));
|
||||
|
||||
|
||||
mockedFiles.when(() -> Files.list(eq(customTempDir)))
|
||||
.thenReturn(Stream.of(ourTempFile3, ourTempFile4, sysTempFile2, sysTempFile3));
|
||||
|
||||
|
||||
mockedFiles.when(() -> Files.list(eq(libreOfficeTempDir)))
|
||||
.thenReturn(Stream.of(ourTempFile5));
|
||||
|
||||
|
||||
mockedFiles.when(() -> Files.list(eq(nestedDir)))
|
||||
.thenReturn(Stream.of(nestedTempFile));
|
||||
|
||||
|
||||
// Configure Files.isDirectory for each path
|
||||
mockedFiles.when(() -> Files.isDirectory(eq(nestedDir))).thenReturn(true);
|
||||
mockedFiles.when(() -> Files.isDirectory(any(Path.class))).thenReturn(false);
|
||||
|
||||
|
||||
// Configure Files.exists to return true for all paths
|
||||
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
||||
|
||||
|
||||
// Configure Files.getLastModifiedTime to return different times based on file names
|
||||
mockedFiles.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
Path path = invocation.getArgument(0);
|
||||
String fileName = path.getFileName().toString();
|
||||
|
||||
|
||||
// For files with "old" in the name, return a timestamp older than maxAgeMillis
|
||||
if (fileName.contains("old")) {
|
||||
return FileTime.fromMillis(System.currentTimeMillis() - 5000000);
|
||||
@ -183,13 +183,13 @@ public class TempFileCleanupServiceTest {
|
||||
return FileTime.fromMillis(System.currentTimeMillis() - 60000); // 1 minute ago
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Configure Files.size to return different sizes based on file names
|
||||
mockedFiles.when(() -> Files.size(any(Path.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
Path path = invocation.getArgument(0);
|
||||
String fileName = path.getFileName().toString();
|
||||
|
||||
|
||||
// Return 0 bytes for the empty file
|
||||
if (fileName.equals("empty.tmp")) {
|
||||
return 0L;
|
||||
@ -199,7 +199,7 @@ public class TempFileCleanupServiceTest {
|
||||
return 1024L; // 1 KB
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// For deleteIfExists, track which files would be deleted
|
||||
mockedFiles.when(() -> Files.deleteIfExists(any(Path.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
@ -207,28 +207,28 @@ public class TempFileCleanupServiceTest {
|
||||
deletedFiles.add(path);
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
// Act - set containerMode to false for this test
|
||||
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
||||
invokeCleanupDirectoryStreaming(customTempDir, false, 0, 3600000);
|
||||
invokeCleanupDirectoryStreaming(libreOfficeTempDir, false, 0, 3600000);
|
||||
|
||||
|
||||
// Assert - Only old temp files and empty files should be deleted
|
||||
assertTrue(deletedFiles.contains(oldTempFile), "Old temp file should be deleted");
|
||||
assertTrue(deletedFiles.contains(emptyFile), "Empty file should be deleted");
|
||||
|
||||
|
||||
// Regular temp files should not be deleted because they're too new
|
||||
assertFalse(deletedFiles.contains(ourTempFile1), "Recent temp file should be preserved");
|
||||
assertFalse(deletedFiles.contains(ourTempFile2), "Recent temp file should be preserved");
|
||||
assertFalse(deletedFiles.contains(ourTempFile3), "Recent temp file should be preserved");
|
||||
assertFalse(deletedFiles.contains(ourTempFile4), "Recent temp file should be preserved");
|
||||
assertFalse(deletedFiles.contains(ourTempFile5), "Recent temp file should be preserved");
|
||||
|
||||
|
||||
// System temp files should not be deleted in non-container mode
|
||||
assertFalse(deletedFiles.contains(sysTempFile1), "System temp file should be preserved in non-container mode");
|
||||
assertFalse(deletedFiles.contains(sysTempFile2), "System temp file should be preserved in non-container mode");
|
||||
assertFalse(deletedFiles.contains(sysTempFile3), "System temp file should be preserved in non-container mode");
|
||||
|
||||
|
||||
// Jetty files and regular files should never be deleted
|
||||
assertFalse(deletedFiles.contains(jettyFile1), "Jetty file should be preserved");
|
||||
assertFalse(deletedFiles.contains(jettyFile2), "File with jetty in name should be preserved");
|
||||
@ -242,10 +242,10 @@ public class TempFileCleanupServiceTest {
|
||||
Path ourTempFile = Files.createFile(systemTempDir.resolve("output_123.pdf"));
|
||||
Path sysTempFile = Files.createFile(systemTempDir.resolve("lu123abc.tmp"));
|
||||
Path regularFile = Files.createFile(systemTempDir.resolve("important.txt"));
|
||||
|
||||
|
||||
// Configure mock registry to say these files aren't registered
|
||||
when(registry.contains(any(File.class))).thenReturn(false);
|
||||
|
||||
|
||||
// The set of files that will be deleted in our test
|
||||
Set<Path> deletedFiles = new HashSet<>();
|
||||
|
||||
@ -254,21 +254,21 @@ public class TempFileCleanupServiceTest {
|
||||
// Mock Files.list for systemTempDir
|
||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
||||
.thenReturn(Stream.of(ourTempFile, sysTempFile, regularFile));
|
||||
|
||||
|
||||
// Configure Files.isDirectory
|
||||
mockedFiles.when(() -> Files.isDirectory(any(Path.class))).thenReturn(false);
|
||||
|
||||
|
||||
// Configure Files.exists
|
||||
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
||||
|
||||
|
||||
// Configure Files.getLastModifiedTime to return recent timestamps
|
||||
mockedFiles.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||
.thenReturn(FileTime.fromMillis(System.currentTimeMillis() - 60000)); // 1 minute ago
|
||||
|
||||
|
||||
// Configure Files.size to return normal size
|
||||
mockedFiles.when(() -> Files.size(any(Path.class)))
|
||||
.thenReturn(1024L); // 1 KB
|
||||
|
||||
|
||||
// For deleteIfExists, track which files would be deleted
|
||||
mockedFiles.when(() -> Files.deleteIfExists(any(Path.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
@ -276,10 +276,10 @@ public class TempFileCleanupServiceTest {
|
||||
deletedFiles.add(path);
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
// Act - set containerMode to true and maxAgeMillis to 0 for container startup cleanup
|
||||
invokeCleanupDirectoryStreaming(systemTempDir, true, 0, 0);
|
||||
|
||||
|
||||
// Assert - In container mode, both our temp files and system temp files should be deleted
|
||||
// regardless of age (when maxAgeMillis is 0)
|
||||
assertTrue(deletedFiles.contains(ourTempFile), "Our temp file should be deleted in container mode");
|
||||
@ -293,10 +293,10 @@ public class TempFileCleanupServiceTest {
|
||||
// Arrange - Create an empty file
|
||||
Path emptyFile = Files.createFile(systemTempDir.resolve("empty.tmp"));
|
||||
Path recentEmptyFile = Files.createFile(systemTempDir.resolve("recent_empty.tmp"));
|
||||
|
||||
|
||||
// Configure mock registry to say these files aren't registered
|
||||
when(registry.contains(any(File.class))).thenReturn(false);
|
||||
|
||||
|
||||
// The set of files that will be deleted in our test
|
||||
Set<Path> deletedFiles = new HashSet<>();
|
||||
|
||||
@ -305,19 +305,19 @@ public class TempFileCleanupServiceTest {
|
||||
// Mock Files.list for systemTempDir
|
||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
||||
.thenReturn(Stream.of(emptyFile, recentEmptyFile));
|
||||
|
||||
|
||||
// Configure Files.isDirectory
|
||||
mockedFiles.when(() -> Files.isDirectory(any(Path.class))).thenReturn(false);
|
||||
|
||||
|
||||
// Configure Files.exists
|
||||
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
||||
|
||||
|
||||
// Configure Files.getLastModifiedTime to return different times based on file names
|
||||
mockedFiles.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
Path path = invocation.getArgument(0);
|
||||
String fileName = path.getFileName().toString();
|
||||
|
||||
|
||||
if (fileName.equals("empty.tmp")) {
|
||||
// More than 5 minutes old
|
||||
return FileTime.fromMillis(System.currentTimeMillis() - 6 * 60 * 1000);
|
||||
@ -326,11 +326,11 @@ public class TempFileCleanupServiceTest {
|
||||
return FileTime.fromMillis(System.currentTimeMillis() - 2 * 60 * 1000);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Configure Files.size to return 0 for empty files
|
||||
mockedFiles.when(() -> Files.size(any(Path.class)))
|
||||
.thenReturn(0L);
|
||||
|
||||
|
||||
// For deleteIfExists, track which files would be deleted
|
||||
mockedFiles.when(() -> Files.deleteIfExists(any(Path.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
@ -338,14 +338,14 @@ public class TempFileCleanupServiceTest {
|
||||
deletedFiles.add(path);
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
// Act
|
||||
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
||||
|
||||
|
||||
// Assert
|
||||
assertTrue(deletedFiles.contains(emptyFile),
|
||||
assertTrue(deletedFiles.contains(emptyFile),
|
||||
"Empty file older than 5 minutes should be deleted");
|
||||
assertFalse(deletedFiles.contains(recentEmptyFile),
|
||||
assertFalse(deletedFiles.contains(recentEmptyFile),
|
||||
"Empty file newer than 5 minutes should not be deleted");
|
||||
}
|
||||
}
|
||||
@ -356,14 +356,14 @@ public class TempFileCleanupServiceTest {
|
||||
Path dir1 = Files.createDirectories(systemTempDir.resolve("dir1"));
|
||||
Path dir2 = Files.createDirectories(dir1.resolve("dir2"));
|
||||
Path dir3 = Files.createDirectories(dir2.resolve("dir3"));
|
||||
|
||||
|
||||
Path tempFile1 = Files.createFile(dir1.resolve("output_1.pdf"));
|
||||
Path tempFile2 = Files.createFile(dir2.resolve("output_2.pdf"));
|
||||
Path tempFile3 = Files.createFile(dir3.resolve("output_old_3.pdf"));
|
||||
|
||||
|
||||
// Configure mock registry to say these files aren't registered
|
||||
when(registry.contains(any(File.class))).thenReturn(false);
|
||||
|
||||
|
||||
// The set of files that will be deleted in our test
|
||||
Set<Path> deletedFiles = new HashSet<>();
|
||||
|
||||
@ -372,16 +372,16 @@ public class TempFileCleanupServiceTest {
|
||||
// Mock Files.list for each directory
|
||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
||||
.thenReturn(Stream.of(dir1));
|
||||
|
||||
|
||||
mockedFiles.when(() -> Files.list(eq(dir1)))
|
||||
.thenReturn(Stream.of(tempFile1, dir2));
|
||||
|
||||
|
||||
mockedFiles.when(() -> Files.list(eq(dir2)))
|
||||
.thenReturn(Stream.of(tempFile2, dir3));
|
||||
|
||||
|
||||
mockedFiles.when(() -> Files.list(eq(dir3)))
|
||||
.thenReturn(Stream.of(tempFile3));
|
||||
|
||||
|
||||
// Configure Files.isDirectory for each path
|
||||
mockedFiles.when(() -> Files.isDirectory(eq(dir1))).thenReturn(true);
|
||||
mockedFiles.when(() -> Files.isDirectory(eq(dir2))).thenReturn(true);
|
||||
@ -389,16 +389,16 @@ public class TempFileCleanupServiceTest {
|
||||
mockedFiles.when(() -> Files.isDirectory(eq(tempFile1))).thenReturn(false);
|
||||
mockedFiles.when(() -> Files.isDirectory(eq(tempFile2))).thenReturn(false);
|
||||
mockedFiles.when(() -> Files.isDirectory(eq(tempFile3))).thenReturn(false);
|
||||
|
||||
|
||||
// Configure Files.exists to return true for all paths
|
||||
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
||||
|
||||
|
||||
// Configure Files.getLastModifiedTime to return different times based on file names
|
||||
mockedFiles.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
Path path = invocation.getArgument(0);
|
||||
String fileName = path.getFileName().toString();
|
||||
|
||||
|
||||
if (fileName.contains("old")) {
|
||||
// Old file
|
||||
return FileTime.fromMillis(System.currentTimeMillis() - 5000000);
|
||||
@ -407,11 +407,11 @@ public class TempFileCleanupServiceTest {
|
||||
return FileTime.fromMillis(System.currentTimeMillis() - 60000);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Configure Files.size to return normal size
|
||||
mockedFiles.when(() -> Files.size(any(Path.class)))
|
||||
.thenReturn(1024L);
|
||||
|
||||
|
||||
// For deleteIfExists, track which files would be deleted
|
||||
mockedFiles.when(() -> Files.deleteIfExists(any(Path.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
@ -419,14 +419,14 @@ public class TempFileCleanupServiceTest {
|
||||
deletedFiles.add(path);
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
// Act
|
||||
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
||||
|
||||
|
||||
// Debug - print what was deleted
|
||||
System.out.println("Deleted files: " + deletedFiles);
|
||||
System.out.println("Looking for: " + tempFile3);
|
||||
|
||||
|
||||
// Assert
|
||||
assertFalse(deletedFiles.contains(tempFile1), "Recent temp file should be preserved");
|
||||
assertFalse(deletedFiles.contains(tempFile2), "Recent temp file should be preserved");
|
||||
@ -437,28 +437,28 @@ public class TempFileCleanupServiceTest {
|
||||
/**
|
||||
* Helper method to invoke the private cleanupDirectoryStreaming method using reflection
|
||||
*/
|
||||
private void invokeCleanupDirectoryStreaming(Path directory, boolean containerMode, int depth, long maxAgeMillis)
|
||||
private void invokeCleanupDirectoryStreaming(Path directory, boolean containerMode, int depth, long maxAgeMillis)
|
||||
throws IOException {
|
||||
try {
|
||||
// Create a consumer that tracks deleted files
|
||||
AtomicInteger deleteCount = new AtomicInteger(0);
|
||||
Consumer<Path> deleteCallback = path -> deleteCount.incrementAndGet();
|
||||
|
||||
|
||||
// Get the method with updated signature
|
||||
var method = TempFileCleanupService.class.getDeclaredMethod(
|
||||
"cleanupDirectoryStreaming",
|
||||
"cleanupDirectoryStreaming",
|
||||
Path.class, boolean.class, int.class, long.class, boolean.class, Consumer.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
|
||||
// Invoke the method with appropriate parameters
|
||||
method.invoke(cleanupService, directory, containerMode, depth, maxAgeMillis, false, deleteCallback);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error invoking cleanupDirectoryStreaming", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Matcher for exact path equality
|
||||
private static Path eq(Path path) {
|
||||
return argThat(arg -> arg != null && arg.equals(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ public class FileToPdfTest {
|
||||
String fileName = "test.html"; // Sample file name indicating an HTML file
|
||||
boolean disableSanitize = false; // Flag to control sanitization
|
||||
TempFileManager tempFileManager = mock(TempFileManager.class); // Mock TempFileManager
|
||||
|
||||
|
||||
// Mock the temp file creation to return real temp files
|
||||
try {
|
||||
when(tempFileManager.createTempFile(anyString()))
|
||||
|
@ -22,7 +22,7 @@ class SpringContextHolderTest {
|
||||
void testSetApplicationContext() {
|
||||
// Act
|
||||
contextHolder.setApplicationContext(mockApplicationContext);
|
||||
|
||||
|
||||
// Assert
|
||||
assertTrue(SpringContextHolder.isInitialized());
|
||||
}
|
||||
@ -33,10 +33,10 @@ class SpringContextHolderTest {
|
||||
contextHolder.setApplicationContext(mockApplicationContext);
|
||||
TestBean expectedBean = new TestBean();
|
||||
when(mockApplicationContext.getBean(TestBean.class)).thenReturn(expectedBean);
|
||||
|
||||
|
||||
// Act
|
||||
TestBean result = SpringContextHolder.getBean(TestBean.class);
|
||||
|
||||
|
||||
// Assert
|
||||
assertSame(expectedBean, result);
|
||||
verify(mockApplicationContext).getBean(TestBean.class);
|
||||
@ -46,10 +46,10 @@ class SpringContextHolderTest {
|
||||
@Test
|
||||
void testGetBean_ApplicationContextNotSet() {
|
||||
// Don't set application context
|
||||
|
||||
|
||||
// Act
|
||||
TestBean result = SpringContextHolder.getBean(TestBean.class);
|
||||
|
||||
|
||||
// Assert
|
||||
assertNull(result);
|
||||
}
|
||||
@ -59,10 +59,10 @@ class SpringContextHolderTest {
|
||||
// Arrange
|
||||
contextHolder.setApplicationContext(mockApplicationContext);
|
||||
when(mockApplicationContext.getBean(TestBean.class)).thenThrow(new org.springframework.beans.BeansException("Bean not found") {});
|
||||
|
||||
|
||||
// Act
|
||||
TestBean result = SpringContextHolder.getBean(TestBean.class);
|
||||
|
||||
|
||||
// Assert
|
||||
assertNull(result);
|
||||
}
|
||||
@ -70,4 +70,4 @@ class SpringContextHolderTest {
|
||||
// Simple test class
|
||||
private static class TestBean {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
588
devGuide/DeveloperGuide.md
Normal file
588
devGuide/DeveloperGuide.md
Normal file
@ -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.
|
66
devGuide/EXCEPTION_HANDLING_GUIDE.md
Normal file
66
devGuide/EXCEPTION_HANDLING_GUIDE.md
Normal file
@ -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.
|
29
devGuide/README.md
Normal file
29
devGuide/README.md
Normal file
@ -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/logs:/logs:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en-US
|
||||
UI_APPNAME: Stirling-PDF-Ultra-lite
|
||||
|
@ -18,7 +18,6 @@ services:
|
||||
- ./stirling/latest/config:/configs:rw
|
||||
- ./stirling/latest/logs:/logs:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
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"
|
||||
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-oauth2-client'
|
||||
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'
|
||||
|
||||
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
|
||||
|
@ -168,7 +168,7 @@
|
||||
<div class="data-form-group">
|
||||
<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)">
|
||||
<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}"
|
||||
th:data-team="${user.team != null ? user.team.name : ''}"
|
||||
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}"
|
||||
# Check for DISABLE_ADDITIONAL_FEATURES and download the appropriate JAR if required
|
||||
if [ "$DISABLE_ADDITIONAL_FEATURES" = "false" ] && [ "$VERSION_TAG" != "alpha" ]; then
|
||||
if [ ! -f app-security.jar ]; then
|
||||
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 [ "$VERSION_TAG" != "alpha" ] && [ "$VERSION_TAG" != "ALPHA" ]; then
|
||||
if [ "$DISABLE_ADDITIONAL_FEATURES" = "false" ] || [ "$DISABLE_ADDITIONAL_FEATURES" = "FALSE" ] || [ "$DOCKER_ENABLE_SECURITY" = "true" ] || [ "$DOCKER_ENABLE_SECURITY" = "TRUE" ]; then
|
||||
if [ ! -f app-security.jar ]; then
|
||||
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 [ $? -ne 0 ]; then
|
||||
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
|
||||
fi
|
||||
# If the first download attempt failed, try without the 'v' prefix
|
||||
if [ $? -ne 0 ]; then
|
||||
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
|
||||
fi
|
||||
|
||||
if [ $? -eq 0 ]; then # checks if curl was successful
|
||||
rm -f app.jar
|
||||
ln -s app-security.jar app.jar
|
||||
chown stirlingpdfuser:stirlingpdfgroup app.jar || true
|
||||
chmod 755 app.jar || true
|
||||
if [ $? -eq 0 ]; then # checks if curl was successful
|
||||
rm -f app.jar
|
||||
ln -s app-security.jar app.jar
|
||||
chown stirlingpdfuser:stirlingpdfgroup app.jar || true
|
||||
chmod 755 app.jar || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
48
scripts/generate_requirements.bat
Normal file
48
scripts/generate_requirements.bat
Normal file
@ -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',
|
||||
'PDFToText.tags',
|
||||
'addPageNumbers.selectText.3',
|
||||
'adminUserSettings.team',
|
||||
'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',
|
||||
'cookieBanner.popUp.acceptAllBtn',
|
||||
'endpointStatistics.top10',
|
||||
'endpointStatistics.top20',
|
||||
'fakeScan.quality.medium',
|
||||
'fileChooser.dragAndDrop',
|
||||
'home.pipeline.title',
|
||||
'lang.afr',
|
||||
@ -235,6 +243,7 @@ ignore = [
|
||||
'pro',
|
||||
'redact.zoom',
|
||||
'sponsor',
|
||||
'team.status',
|
||||
'text',
|
||||
'validateSignature.cert.bits',
|
||||
'validateSignature.cert.version',
|
||||
@ -420,27 +429,17 @@ ignore = [
|
||||
|
||||
[hu_HU]
|
||||
ignore = [
|
||||
'AddStampRequest.alphabet',
|
||||
'AddStampRequest.position',
|
||||
'adminUserSettings.admin',
|
||||
'alphabet',
|
||||
'audit.dashboard.export.json',
|
||||
'audit.dashboard.modal.id',
|
||||
'audit.dashboard.table.id',
|
||||
'certSign.name',
|
||||
'cookieBanner.popUp.acceptAllBtn',
|
||||
'endpointStatistics.top10',
|
||||
'endpointStatistics.top20',
|
||||
'home.pipeline.title',
|
||||
'language.direction',
|
||||
'licenses.version',
|
||||
'poweredBy',
|
||||
'pipeline.title',
|
||||
'pipelineOptions.pipelineHeader',
|
||||
'pro',
|
||||
'sponsor',
|
||||
'text',
|
||||
'validateSignature.cert.bits',
|
||||
'validateSignature.cert.version',
|
||||
'validateSignature.status',
|
||||
'watermark.type.1',
|
||||
'showJS.tags',
|
||||
]
|
||||
|
||||
[id_ID]
|
||||
@ -495,7 +494,6 @@ ignore = [
|
||||
[it_IT]
|
||||
ignore = [
|
||||
'lang.asm',
|
||||
'lang.aze',
|
||||
'lang.ceb',
|
||||
'lang.chr',
|
||||
'lang.div',
|
||||
@ -858,9 +856,29 @@ ignore = [
|
||||
|
||||
[sr_LATN_RS]
|
||||
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',
|
||||
'licenses.version',
|
||||
'poweredBy',
|
||||
'pro',
|
||||
'showJS.tags',
|
||||
'team.status',
|
||||
'validateSignature.status',
|
||||
]
|
||||
|
||||
[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 = auto_rotate(cropped_image, angle_threshold)
|
||||
|
||||
# Remove the added border
|
||||
cropped_image = cropped_image[border_size:-border_size, border_size:-border_size]
|
||||
# Remove the added border, but ensure we don't create an empty image
|
||||
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")
|
||||
cv2.imwrite(output_path, cropped_image)
|
||||
|
@ -62,7 +62,7 @@ dependencies {
|
||||
exclude group: 'com.google.code.gson', module: 'gson'
|
||||
}
|
||||
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
|
||||
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 ':proprietary:jar'
|
||||
|
@ -21,6 +21,8 @@ public class EndpointConfiguration {
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private Map<String, Boolean> endpointStatuses = 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;
|
||||
|
||||
public EndpointConfiguration(
|
||||
@ -34,13 +36,14 @@ public class EndpointConfiguration {
|
||||
|
||||
public void enableEndpoint(String endpoint) {
|
||||
endpointStatuses.put(endpoint, true);
|
||||
log.debug("Enabled endpoint: {}", endpoint);
|
||||
}
|
||||
|
||||
public void disableEndpoint(String endpoint) {
|
||||
if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
|
||||
log.debug("Disabling {}", endpoint);
|
||||
endpointStatuses.put(endpoint, false);
|
||||
if (!Boolean.FALSE.equals(endpointStatuses.get(endpoint))) {
|
||||
log.debug("Disabling endpoint: {}", endpoint);
|
||||
}
|
||||
endpointStatuses.put(endpoint, false);
|
||||
}
|
||||
|
||||
public Map<String, Boolean> getEndpointStatuses() {
|
||||
@ -48,25 +51,96 @@ public class EndpointConfiguration {
|
||||
}
|
||||
|
||||
public boolean isEndpointEnabled(String endpoint) {
|
||||
String original = endpoint;
|
||||
if (endpoint.startsWith("/")) {
|
||||
endpoint = endpoint.substring(1);
|
||||
}
|
||||
return endpointStatuses.getOrDefault(endpoint, true);
|
||||
}
|
||||
|
||||
public boolean isGroupEnabled(String group) {
|
||||
Set<String> endpoints = endpointGroups.get(group);
|
||||
if (endpoints == null || endpoints.isEmpty()) {
|
||||
log.debug("Group '{}' does not exist or has no endpoints", group);
|
||||
// Rule 1: Explicit flag wins - if disabled via disableEndpoint(), stay disabled
|
||||
Boolean explicitStatus = endpointStatuses.get(endpoint);
|
||||
if (Boolean.FALSE.equals(explicitStatus)) {
|
||||
log.debug("isEndpointEnabled('{}') -> false (explicitly disabled)", original);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (String endpoint : endpoints) {
|
||||
if (!isEndpointEnabled(endpoint)) {
|
||||
// Rule 2: Functional-group override - check if endpoint belongs to any disabled functional
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@ -74,38 +148,84 @@ public class EndpointConfiguration {
|
||||
endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint);
|
||||
}
|
||||
|
||||
public void enableGroup(String group) {
|
||||
Set<String> endpoints = endpointGroups.get(group);
|
||||
if (endpoints != null) {
|
||||
for (String endpoint : endpoints) {
|
||||
enableEndpoint(endpoint);
|
||||
}
|
||||
}
|
||||
public void addEndpointAlternative(String endpoint, String toolGroup) {
|
||||
endpointAlternatives.computeIfAbsent(endpoint, k -> new HashSet<>()).add(toolGroup);
|
||||
}
|
||||
|
||||
public void disableGroup(String group) {
|
||||
Set<String> endpoints = endpointGroups.get(group);
|
||||
if (endpoints != null) {
|
||||
for (String endpoint : endpoints) {
|
||||
disableEndpoint(endpoint);
|
||||
if (disabledGroups.add(group)) {
|
||||
if (isToolGroup(group)) {
|
||||
log.debug(
|
||||
"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() {
|
||||
List<String> disabledList =
|
||||
endpointStatuses.entrySet().stream()
|
||||
.filter(entry -> !entry.getValue()) // only get disabled endpoints (value
|
||||
// is false)
|
||||
.map(Map.Entry::getKey)
|
||||
// Get all unique endpoints across all groups
|
||||
Set<String> allEndpoints =
|
||||
endpointGroups.values().stream()
|
||||
.flatMap(Set::stream)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
// Check which endpoints are actually disabled (functionally unavailable)
|
||||
List<String> functionallyDisabledEndpoints =
|
||||
allEndpoints.stream()
|
||||
.filter(endpoint -> !isEndpointEnabled(endpoint))
|
||||
.sorted()
|
||||
.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(
|
||||
"Total disabled endpoints: {}. Disabled endpoints: {}",
|
||||
disabledList.size(),
|
||||
String.join(", ", disabledList));
|
||||
functionallyDisabledEndpoints.size(),
|
||||
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
|
||||
addEndpointToGroup("Unoconvert", "file-to-pdf");
|
||||
|
||||
addEndpointToGroup("tesseract", "ocr-pdf");
|
||||
|
||||
// Java
|
||||
addEndpointToGroup("Java", "merge-pdfs");
|
||||
addEndpointToGroup("Java", "remove-pages");
|
||||
@ -254,6 +372,7 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Java", "remove-image-pdf");
|
||||
addEndpointToGroup("Java", "pdf-to-markdown");
|
||||
addEndpointToGroup("Java", "add-attachments");
|
||||
addEndpointToGroup("Java", "compress-pdf");
|
||||
|
||||
// Javascript
|
||||
addEndpointToGroup("Javascript", "pdf-organizer");
|
||||
@ -261,8 +380,42 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Javascript", "compare");
|
||||
addEndpointToGroup("Javascript", "adjust-contrast");
|
||||
|
||||
// qpdf dependent endpoints
|
||||
/* qpdf */
|
||||
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
|
||||
addEndpointToGroup("Weasyprint", "html-to-pdf");
|
||||
@ -304,4 +457,43 @@ public class EndpointConfiguration {
|
||||
public Set<String> getEndpointsForGroup(String group) {
|
||||
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<>() {
|
||||
|
||||
{
|
||||
put("gs", List.of("Ghostscript"));
|
||||
put("ocrmypdf", List.of("OCRmyPDF"));
|
||||
put("soffice", List.of("LibreOffice"));
|
||||
put(weasyprintPath, List.of("Weasyprint"));
|
||||
put("pdftohtml", List.of("Pdftohtml"));
|
||||
@ -109,6 +111,8 @@ public class ExternalAppDepConfig {
|
||||
@PostConstruct
|
||||
public void checkDependencies() {
|
||||
// Check core dependencies
|
||||
checkDependencyAndDisableGroup("gs");
|
||||
checkDependencyAndDisableGroup("ocrmypdf");
|
||||
checkDependencyAndDisableGroup("tesseract");
|
||||
checkDependencyAndDisableGroup("soffice");
|
||||
checkDependencyAndDisableGroup("qpdf");
|
||||
|
@ -26,8 +26,6 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
registry.addResourceHandler("/**")
|
||||
.addResourceLocations(
|
||||
"file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/");
|
||||
registry.addResourceHandler("/js/**").addResourceLocations("classpath:/static/js/");
|
||||
registry.addResourceHandler("/css/**").addResourceLocations("classpath:/static/css/");
|
||||
// .setCachePeriod(0); // Optional: disable caching
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,9 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.PdfErrorUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@ -146,7 +148,7 @@ public class MergeController {
|
||||
try (PDDocument doc = pdfDocumentFactory.load(file)) {
|
||||
pageIndex += doc.getNumberOfPages();
|
||||
} 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
|
||||
}
|
||||
}
|
||||
@ -189,8 +191,17 @@ public class MergeController {
|
||||
mergedTempFile = Files.createTempFile("merged-", ".pdf").toFile();
|
||||
mergerUtility.setDestinationFileName(mergedTempFile.getAbsolutePath());
|
||||
|
||||
mergerUtility.mergeDocuments(
|
||||
pdfDocumentFactory.getStreamCacheFunction(totalSize)); // Merge the documents
|
||||
try {
|
||||
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
|
||||
mergedDocument = pdfDocumentFactory.load(mergedTempFile);
|
||||
@ -229,7 +240,11 @@ public class MergeController {
|
||||
baos, mergedFileName); // Return the modified PDF
|
||||
|
||||
} 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;
|
||||
} finally {
|
||||
if (mergedDocument != null) {
|
||||
|
@ -25,6 +25,7 @@ import stirling.software.SPDF.model.SortTypes;
|
||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@ -288,8 +289,8 @@ public class RearrangePagesPDFController {
|
||||
.replaceFirst("[.][^.]+$", "")
|
||||
+ "_rearranged.pdf");
|
||||
} catch (IOException e) {
|
||||
log.error("Failed rearranging documents", e);
|
||||
return null;
|
||||
ExceptionUtils.logException("document rearrangement", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@ -43,7 +44,8 @@ public class RotationController {
|
||||
|
||||
// Validate the angle is a multiple of 90
|
||||
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
|
||||
|
@ -27,6 +27,7 @@ import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.ScalePagesRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@ -120,9 +121,7 @@ public class ScalePagesController {
|
||||
return sizeMap.get(targetPDRectangle);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid PDRectangle. It must be one of the following: A0, A1, A2, A3, A4, A5, A6,"
|
||||
+ " LETTER, LEGAL, KEEP");
|
||||
throw ExceptionUtils.createInvalidPageSizeException(targetPDRectangle);
|
||||
}
|
||||
|
||||
private Map<String, PDRectangle> getSizeMap() {
|
||||
|
@ -29,6 +29,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@ -71,7 +72,7 @@ public class SplitPDFController {
|
||||
pageNumbers.add(totalPages - 1);
|
||||
}
|
||||
|
||||
log.info(
|
||||
log.debug(
|
||||
"Splitting PDF into pages: {}",
|
||||
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
|
||||
|
||||
@ -84,7 +85,7 @@ public class SplitPDFController {
|
||||
for (int i = previousPageNumber; i <= splitPoint; i++) {
|
||||
PDPage page = document.getPage(i);
|
||||
splitDocument.addPage(page);
|
||||
log.info("Adding page {} to split document", i);
|
||||
log.debug("Adding page {} to split document", i);
|
||||
}
|
||||
previousPageNumber = splitPoint + 1;
|
||||
|
||||
@ -96,7 +97,7 @@ public class SplitPDFController {
|
||||
|
||||
splitDocumentsBoas.add(baos);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed splitting documents and saving them", e);
|
||||
ExceptionUtils.logException("document splitting and saving", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@ -122,14 +123,14 @@ public class SplitPDFController {
|
||||
zipOut.write(pdf);
|
||||
zipOut.closeEntry();
|
||||
|
||||
log.info("Wrote split document {} to zip file", fileName);
|
||||
log.debug("Wrote split document {} to zip file", fileName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed writing to zip", 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);
|
||||
Files.deleteIfExists(zipFile);
|
||||
|
||||
|
@ -35,6 +35,7 @@ import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
|
||||
import stirling.software.common.model.PdfMetadata;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.service.PdfMetadataService;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@ -131,7 +132,8 @@ public class SplitPdfByChaptersController {
|
||||
Integer bookmarkLevel =
|
||||
request.getBookmarkLevel(); // levels start from 0 (top most bookmarks)
|
||||
if (bookmarkLevel < 0) {
|
||||
throw new IllegalArgumentException("Invalid bookmark level");
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.invalidArgument", "Invalid argument: {0}", "bookmark level");
|
||||
}
|
||||
sourceDocument = pdfDocumentFactory.load(file);
|
||||
|
||||
@ -139,7 +141,8 @@ public class SplitPdfByChaptersController {
|
||||
|
||||
if (outline == null) {
|
||||
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<>();
|
||||
try {
|
||||
@ -156,7 +159,7 @@ public class SplitPdfByChaptersController {
|
||||
Bookmark lastBookmark = bookmarks.get(bookmarks.size() - 1);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Unable to extract outline items", e);
|
||||
ExceptionUtils.logException("outline extraction", e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body("Unable to extract outline items".getBytes());
|
||||
}
|
||||
@ -252,7 +255,7 @@ public class SplitPdfByChaptersController {
|
||||
zipOut.write(pdf);
|
||||
zipOut.closeEntry();
|
||||
|
||||
log.info("Wrote split document {} to zip file", fileName);
|
||||
log.debug("Wrote split document {} to zip file", fileName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed writing to zip", e);
|
||||
@ -280,7 +283,7 @@ public class SplitPdfByChaptersController {
|
||||
i++) {
|
||||
PDPage page = sourceDocument.getPage(i);
|
||||
splitDocument.addPage(page);
|
||||
log.info("Adding page {} to split document", i);
|
||||
log.debug("Adding page {} to split document", i);
|
||||
}
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
if (includeMetadata) {
|
||||
@ -291,7 +294,7 @@ public class SplitPdfByChaptersController {
|
||||
|
||||
splitDocumentsBoas.add(baos);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed splitting documents and saving them", e);
|
||||
ExceptionUtils.logException("document splitting and saving", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@ -96,13 +97,15 @@ public class SplitPdfBySizeController {
|
||||
handleSplitByDocCount(sourceDocument, documentCount, zipOut, filename);
|
||||
} else {
|
||||
log.error("Invalid split type: {}", type);
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid argument for split type: " + type);
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.invalidArgument",
|
||||
"Invalid argument: {0}",
|
||||
"split type: " + type);
|
||||
}
|
||||
|
||||
log.debug("PDF splitting completed successfully");
|
||||
} catch (Exception e) {
|
||||
log.error("Error loading or processing PDF document", e);
|
||||
ExceptionUtils.logException("PDF document loading or processing", e);
|
||||
throw e;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
@ -111,7 +114,7 @@ public class SplitPdfBySizeController {
|
||||
}
|
||||
|
||||
} 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
|
||||
} finally {
|
||||
try {
|
||||
@ -275,8 +278,8 @@ public class SplitPdfBySizeController {
|
||||
currentDoc = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
|
||||
log.debug("Successfully created initial output document");
|
||||
} catch (Exception e) {
|
||||
log.error("Error creating initial output document", e);
|
||||
throw new IOException("Failed to create initial output document", e);
|
||||
ExceptionUtils.logException("initial output document creation", e);
|
||||
throw ExceptionUtils.createFileProcessingException("split", e);
|
||||
}
|
||||
|
||||
int fileIndex = 1;
|
||||
@ -295,7 +298,7 @@ public class SplitPdfBySizeController {
|
||||
log.debug("Successfully added page {} to current document", pageIndex);
|
||||
} catch (Exception 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++;
|
||||
@ -320,7 +323,7 @@ public class SplitPdfBySizeController {
|
||||
log.debug("Successfully created new document");
|
||||
} catch (Exception 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;
|
||||
@ -329,7 +332,7 @@ public class SplitPdfBySizeController {
|
||||
}
|
||||
} catch (Exception 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
|
||||
@ -351,7 +354,7 @@ public class SplitPdfBySizeController {
|
||||
}
|
||||
} catch (Exception 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 {
|
||||
try {
|
||||
log.debug("Closing final document");
|
||||
@ -390,7 +393,7 @@ public class SplitPdfBySizeController {
|
||||
log.debug("Successfully created document {} of {}", i + 1, documentCount);
|
||||
} catch (Exception 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);
|
||||
@ -408,7 +411,7 @@ public class SplitPdfBySizeController {
|
||||
currentPageIndex++;
|
||||
} catch (Exception 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());
|
||||
} catch (Exception 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 {
|
||||
@ -465,7 +468,7 @@ public class SplitPdfBySizeController {
|
||||
log.debug("Successfully added document part {} to ZIP", index);
|
||||
} catch (Exception 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,17 +70,17 @@ public class ToSinglePageController {
|
||||
float yOffset = totalHeight;
|
||||
|
||||
// For each page, copy its content to the new page at the correct offset
|
||||
int pageIndex = 0;
|
||||
for (PDPage page : sourceDocument.getPages()) {
|
||||
PDFormXObject form =
|
||||
layerUtility.importPageAsForm(
|
||||
sourceDocument, sourceDocument.getPages().indexOf(page));
|
||||
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, pageIndex);
|
||||
AffineTransform af =
|
||||
AffineTransform.getTranslateInstance(
|
||||
0, yOffset - page.getMediaBox().getHeight());
|
||||
layerUtility.wrapInSaveRestore(newPage);
|
||||
String defaultLayerName = "Layer" + sourceDocument.getPages().indexOf(page);
|
||||
String defaultLayerName = "Layer" + pageIndex;
|
||||
layerUtility.appendFormAsLayer(newPage, form, af, defaultLayerName);
|
||||
yOffset -= page.getMediaBox().getHeight();
|
||||
pageIndex++;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
@ -17,6 +17,7 @@ import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.FileToPdf;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@ -46,14 +47,14 @@ public class ConvertHtmlToPDF {
|
||||
MultipartFile fileInput = request.getFileInput();
|
||||
|
||||
if (fileInput == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Please provide an HTML or ZIP file for conversion.");
|
||||
throw ExceptionUtils.createHtmlFileRequiredException();
|
||||
}
|
||||
|
||||
String originalFilename = Filenames.toSimpleFileName(fileInput.getOriginalFilename());
|
||||
if (originalFilename == null
|
||||
|| (!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 =
|
||||
|
@ -34,6 +34,7 @@ import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.CheckProgramInstall;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.PdfUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
@ -104,7 +105,7 @@ public class ConvertImgPDFController {
|
||||
log.error("resultant bytes for {} is null, error converting ", filename);
|
||||
}
|
||||
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)
|
||||
&& CheckProgramInstall.isPythonAvailable()) {
|
||||
// Write the output stream to a temp file
|
||||
|
@ -27,6 +27,7 @@ import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.api.GeneralFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.FileToPdf;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@ -55,12 +56,14 @@ public class ConvertMarkdownToPdf {
|
||||
MultipartFile fileInput = generalFile.getFileInput();
|
||||
|
||||
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());
|
||||
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
|
||||
|
@ -67,6 +67,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
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.ProcessExecutorResult;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@ -90,7 +91,7 @@ public class ConvertPDFToPDFA {
|
||||
// Validate input file type
|
||||
if (!"application/pdf".equals(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
|
||||
@ -241,15 +242,13 @@ public class ConvertPDFToPDFA {
|
||||
|
||||
if (returnCode.getRc() != 0) {
|
||||
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
|
||||
File[] outputFiles = tempOutputDir.toFile().listFiles();
|
||||
if (outputFiles == null || outputFiles.length != 1) {
|
||||
throw new RuntimeException(
|
||||
"Expected one output PDF, found "
|
||||
+ (outputFiles == null ? "none" : outputFiles.length));
|
||||
throw ExceptionUtils.createPdfaConversionFailedException();
|
||||
}
|
||||
return outputFiles[0].toPath();
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
@ -50,16 +51,19 @@ public class ConvertWebsiteToPDF {
|
||||
String URL = request.getUrlInput();
|
||||
|
||||
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
|
||||
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
|
||||
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;
|
||||
|
@ -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.PageSizeRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.PdfUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@ -96,7 +97,7 @@ public class FilterController {
|
||||
valid = actualPageCount < pageCount;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
||||
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
|
||||
}
|
||||
|
||||
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||
@ -139,7 +140,7 @@ public class FilterController {
|
||||
valid = actualArea < standardArea;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
||||
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
|
||||
}
|
||||
|
||||
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||
@ -172,7 +173,7 @@ public class FilterController {
|
||||
valid = actualFileSize < fileSize;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
||||
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
|
||||
}
|
||||
|
||||
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||
@ -208,7 +209,7 @@ public class FilterController {
|
||||
valid = actualRotation < rotation;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
||||
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
|
||||
}
|
||||
|
||||
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||
|
@ -47,11 +47,13 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
@ -61,16 +63,18 @@ import stirling.software.common.util.WebResponseUtils;
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Slf4j
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
@RequiredArgsConstructor
|
||||
public class CompressController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final boolean qpdfEnabled;
|
||||
private final EndpointConfiguration endpointConfiguration;
|
||||
|
||||
public CompressController(
|
||||
CustomPDFDocumentFactory pdfDocumentFactory,
|
||||
EndpointConfiguration endpointConfiguration) {
|
||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||
this.qpdfEnabled = endpointConfiguration.isGroupEnabled("qpdf");
|
||||
private boolean isQpdfEnabled() {
|
||||
return endpointConfiguration.isGroupEnabled("qpdf");
|
||||
}
|
||||
|
||||
private boolean isGhostscriptEnabled() {
|
||||
return endpointConfiguration.isGroupEnabled("Ghostscript");
|
||||
}
|
||||
|
||||
@Data
|
||||
@ -597,12 +601,12 @@ public class CompressController {
|
||||
if (bytesRead > 0) {
|
||||
byte[] dataToHash =
|
||||
bytesRead == buffer.length ? buffer : Arrays.copyOf(buffer, bytesRead);
|
||||
return bytesToHexString(generatMD5(dataToHash));
|
||||
return bytesToHexString(generateMD5(dataToHash));
|
||||
}
|
||||
return "empty-stream";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error generating image hash", e);
|
||||
ExceptionUtils.logException("image hash generation", e);
|
||||
return "fallback-" + System.identityHashCode(image);
|
||||
}
|
||||
}
|
||||
@ -615,12 +619,12 @@ public class CompressController {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private byte[] generatMD5(byte[] data) throws IOException {
|
||||
private byte[] generateMD5(byte[] data) throws IOException {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
return md.digest(data); // Get the MD5 hash of the image bytes
|
||||
} 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 imageCompressionApplied = false;
|
||||
boolean qpdfCompressionApplied = false;
|
||||
|
||||
if (qpdfEnabled && optimizeLevel <= 3) {
|
||||
optimizeLevel = 4;
|
||||
}
|
||||
boolean externalCompressionApplied = false;
|
||||
|
||||
while (!sizeMet && optimizeLevel <= 9) {
|
||||
// Apply image compression for levels 4-9
|
||||
if ((optimizeLevel >= 3 || Boolean.TRUE.equals(convertToGrayscale))
|
||||
&& !imageCompressionApplied) {
|
||||
double scaleFactor = getScaleFactorForLevel(optimizeLevel);
|
||||
float jpegQuality = getJpegQualityForLevel(optimizeLevel);
|
||||
// Apply external compression first
|
||||
if (!externalCompressionApplied) {
|
||||
boolean ghostscriptSuccess = false;
|
||||
|
||||
// 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 =
|
||||
compressImagesInPDF(
|
||||
currentFile,
|
||||
scaleFactor,
|
||||
jpegQuality,
|
||||
0.7f, // Default JPEG quality
|
||||
Boolean.TRUE.equals(convertToGrayscale));
|
||||
|
||||
tempFiles.add(compressedImageFile);
|
||||
@ -723,18 +771,6 @@ public class CompressController {
|
||||
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
|
||||
long outputFileSize = Files.size(currentFile);
|
||||
if (outputFileSize <= expectedOutputSize || !autoMode) {
|
||||
@ -754,7 +790,7 @@ public class CompressController {
|
||||
} else {
|
||||
// Reset flags for next iteration with higher optimization level
|
||||
imageCompressionApplied = false;
|
||||
qpdfCompressionApplied = false;
|
||||
externalCompressionApplied = false;
|
||||
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
|
||||
private void applyQpdfCompression(
|
||||
OptimizePdfRequest request, int optimizeLevel, Path currentFile, List<Path> tempFiles)
|
||||
|
@ -26,6 +26,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@ -134,7 +135,7 @@ public class DecompressPdfController {
|
||||
stream.setInt(COSName.LENGTH, decompressedBytes.length);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Error decompressing stream", e);
|
||||
ExceptionUtils.logException("stream decompression", e);
|
||||
// Continue processing other streams even if this one fails
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.CheckProgramInstall;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@ -72,7 +73,8 @@ public class ExtractImageScansController {
|
||||
List<Path> tempDirs = new ArrayList<>();
|
||||
|
||||
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();
|
||||
|
@ -41,6 +41,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.PDFExtractImagesRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.ImageProcessingUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@ -90,39 +91,57 @@ public class ExtractImagesController {
|
||||
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
|
||||
Set<Future<Void>> futures = new HashSet<>();
|
||||
|
||||
// Iterate over each page
|
||||
for (int pgNum = 0; pgNum < document.getPages().getCount(); pgNum++) {
|
||||
PDPage page = document.getPage(pgNum);
|
||||
Future<Void> future =
|
||||
executor.submit(
|
||||
() -> {
|
||||
// Use the page number directly from the iterator, so no need to
|
||||
// calculate manually
|
||||
int pageNum = document.getPages().indexOf(page) + 1;
|
||||
// Safely iterate over each page, handling corrupt PDFs where page count might be wrong
|
||||
try {
|
||||
int pageCount = document.getPages().getCount();
|
||||
log.debug("Document reports {} pages", pageCount);
|
||||
|
||||
try {
|
||||
// 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());
|
||||
}
|
||||
int consecutiveFailures = 0;
|
||||
|
||||
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
|
||||
futures.add(future);
|
||||
return null; // Callable requires a return type
|
||||
});
|
||||
|
||||
// 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
|
||||
@ -180,34 +199,39 @@ public class ExtractImagesController {
|
||||
}
|
||||
int count = 1;
|
||||
for (COSName name : page.getResources().getXObjectNames()) {
|
||||
if (page.getResources().isImageXObject(name)) {
|
||||
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
|
||||
if (!allowDuplicates) {
|
||||
byte[] data = ImageProcessingUtils.getImageData(image.getImage());
|
||||
byte[] imageHash = md.digest(data);
|
||||
synchronized (processedImages) {
|
||||
if (processedImages.stream()
|
||||
.anyMatch(hash -> Arrays.equals(hash, imageHash))) {
|
||||
continue; // Skip already processed images
|
||||
try {
|
||||
if (page.getResources().isImageXObject(name)) {
|
||||
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
|
||||
if (!allowDuplicates) {
|
||||
byte[] data = ImageProcessingUtils.getImageData(image.getImage());
|
||||
byte[] imageHash = md.digest(data);
|
||||
synchronized (processedImages) {
|
||||
if (processedImages.stream()
|
||||
.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();
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
ExceptionUtils.logException("image extraction", e);
|
||||
throw ExceptionUtils.handlePdfException(e, "during image extraction");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,11 @@ public class FakeScanController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
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")
|
||||
@Operation(
|
||||
summary = "Convert PDF to look like a scanned document",
|
||||
@ -82,8 +87,46 @@ public class FakeScanController {
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
|
||||
for (int i = 0; i < document.getNumberOfPages(); i++) {
|
||||
// Render page to image with specified resolution
|
||||
BufferedImage image = pdfRenderer.renderImageWithDPI(i, resolution);
|
||||
// Get page dimensions to calculate safe 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
|
||||
BufferedImage processed;
|
||||
|
@ -2,6 +2,7 @@ package stirling.software.SPDF.controller.api.misc;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.zip.ZipEntry;
|
||||
@ -29,12 +30,17 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
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.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@ -46,6 +52,15 @@ public class OCRController {
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
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 */
|
||||
public List<String> getAvailableTesseractLanguages() {
|
||||
@ -63,39 +78,261 @@ public class OCRController {
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
|
||||
@Operation(
|
||||
summary = "Process PDF files with OCR using Tesseract",
|
||||
summary = "Process a PDF file with OCR",
|
||||
description =
|
||||
"Takes a PDF file as input, performs OCR using specified languages and OCR type"
|
||||
+ " (skip-text/force-ocr), and returns the processed PDF. Input:PDF"
|
||||
+ " Output:PDF Type:SISO")
|
||||
"This endpoint processes a PDF file using OCR (Optical Character Recognition). "
|
||||
+ "Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType, and removeImagesAfter options. "
|
||||
+ "Uses OCRmyPDF if available, falls back to Tesseract. Input:PDF Output:PDF Type:SI-Conditional")
|
||||
public ResponseEntity<byte[]> processPdfWithOCR(
|
||||
@ModelAttribute ProcessPdfWithOcrRequest request)
|
||||
throws IOException, InterruptedException {
|
||||
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 ocrRenderType = request.getOcrRenderType();
|
||||
Boolean removeImagesAfter = request.isRemoveImagesAfter();
|
||||
|
||||
// Create a temp directory using TempFileManager directly
|
||||
Path tempDirPath = tempFileManager.createTempDirectory();
|
||||
File tempDir = tempDirPath.toFile();
|
||||
if (selectedLanguages == null || selectedLanguages.isEmpty()) {
|
||||
throw ExceptionUtils.createOcrLanguageRequiredException();
|
||||
}
|
||||
|
||||
try {
|
||||
File tempInputFile = new File(tempDir, "input.pdf");
|
||||
File tempOutputDir = new File(tempDir, "output");
|
||||
File tempImagesDir = new File(tempDir, "images");
|
||||
File finalOutputFile = new File(tempDir, "final_output.pdf");
|
||||
if (!"hocr".equals(ocrRenderType) && !"sandwich".equals(ocrRenderType)) {
|
||||
throw new IOException("ocrRenderType wrong");
|
||||
}
|
||||
|
||||
// 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
|
||||
tempOutputDir.mkdirs();
|
||||
tempImagesDir.mkdirs();
|
||||
|
||||
// Save input file
|
||||
inputFile.transferTo(tempInputFile);
|
||||
|
||||
PDFMergerUtility merger = new PDFMergerUtility();
|
||||
merger.setDestinationFileName(finalOutputFile.toString());
|
||||
|
||||
try (PDDocument document = pdfDocumentFactory.load(tempInputFile)) {
|
||||
try (PDDocument document = pdfDocumentFactory.load(tempInputFile.toFile())) {
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
int pageCount = document.getNumberOfPages();
|
||||
|
||||
@ -135,35 +372,24 @@ public class OCRController {
|
||||
new File(tempOutputDir, String.format("page_%d", pageNum))
|
||||
.toString());
|
||||
command.add("-l");
|
||||
command.add(String.join("+", languages));
|
||||
// Always output PDF
|
||||
command.add("pdf");
|
||||
command.add(String.join("+", selectedLanguages));
|
||||
command.add("pdf"); // Always output PDF
|
||||
|
||||
// Use ProcessExecutor to run tesseract command
|
||||
try {
|
||||
ProcessExecutorResult result =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.TESSERACT)
|
||||
.runCommandWithOutputHandling(command);
|
||||
ProcessExecutorResult result =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.TESSERACT)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
log.debug(
|
||||
"Tesseract OCR completed for page {} with exit code {}",
|
||||
pageNum,
|
||||
if (result.getRc() != 0) {
|
||||
throw ExceptionUtils.createRuntimeException(
|
||||
"error.commandFailed",
|
||||
"{0} command failed with exit code: {1}",
|
||||
null,
|
||||
"Tesseract",
|
||||
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 {
|
||||
// Save original page without OCR
|
||||
try (PDDocument pageDoc = new PDDocument()) {
|
||||
@ -178,40 +404,11 @@ public class OCRController {
|
||||
// Merge all pages into final PDF
|
||||
merger.mergeDocuments(null);
|
||||
|
||||
// Read the final PDF file
|
||||
byte[] pdfContent = java.nio.file.Files.readAllBytes(finalOutputFile.toPath());
|
||||
String outputFilename =
|
||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "")
|
||||
+ "_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();
|
||||
// Copy final output to the expected location
|
||||
Files.copy(
|
||||
finalOutputFile.toPath(),
|
||||
tempOutputFile,
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,9 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
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.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
@ -28,17 +30,27 @@ import stirling.software.common.util.WebResponseUtils;
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class RepairController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
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")
|
||||
@Operation(
|
||||
summary = "Repair a PDF file",
|
||||
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"
|
||||
+ " returned as a response. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile file)
|
||||
@ -46,25 +58,71 @@ public class RepairController {
|
||||
MultipartFile inputFile = file.getFileInput();
|
||||
|
||||
// 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
|
||||
inputFile.transferTo(tempFile.getFile());
|
||||
inputFile.transferTo(tempInputFile.getFile());
|
||||
|
||||
List<String> command = new ArrayList<>();
|
||||
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());
|
||||
boolean repairSuccess = false;
|
||||
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF)
|
||||
.runCommandWithOutputHandling(command);
|
||||
// Try Ghostscript first if available
|
||||
if (isGhostscriptEnabled()) {
|
||||
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
|
||||
byte[] pdfBytes = pdfDocumentFactory.loadToBytes(tempFile.getFile());
|
||||
ProcessExecutorResult gsResult =
|
||||
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 =
|
||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "")
|
||||
|
@ -150,11 +150,22 @@ public class PipelineProcessor {
|
||||
}
|
||||
}
|
||||
if (!hasInputFileType) {
|
||||
String filename = file.getFilename();
|
||||
String providedExtension = "no extension";
|
||||
if (filename != null && filename.contains(".")) {
|
||||
providedExtension =
|
||||
filename.substring(filename.lastIndexOf(".")).toLowerCase();
|
||||
}
|
||||
|
||||
logPrintStream.println(
|
||||
"No files with extension "
|
||||
+ String.join(", ", inputFileTypes)
|
||||
+ " found for operation "
|
||||
+ operation);
|
||||
+ operation
|
||||
+ ". Provided file '"
|
||||
+ filename
|
||||
+ "' has extension: "
|
||||
+ providedExtension);
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
@ -203,11 +214,32 @@ public class PipelineProcessor {
|
||||
hasErrors = true;
|
||||
}
|
||||
} 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(
|
||||
"No files with extension "
|
||||
+ String.join(", ", inputFileTypes)
|
||||
+ " found for multi-input operation "
|
||||
+ operation);
|
||||
+ operation
|
||||
+ ". Provided files have extensions: "
|
||||
+ String.join(", ", providedExtensions)
|
||||
+ " (total files: "
|
||||
+ outputFiles.size()
|
||||
+ ")");
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +65,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
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.tags.Tag;
|
||||
|
||||
@ -73,6 +74,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@ -132,7 +134,7 @@ public class CertSignController {
|
||||
}
|
||||
doc.saveIncremental(output);
|
||||
} 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;
|
||||
Boolean showLogo = request.getShowLogo();
|
||||
|
||||
if (certType == null) {
|
||||
throw new IllegalArgumentException("Cert type must be provided");
|
||||
if (StringUtils.isBlank(certType)) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.optionsNotSpecified",
|
||||
"{0} options are not specified",
|
||||
"certificate type");
|
||||
}
|
||||
|
||||
KeyStore ks = null;
|
||||
@ -189,7 +194,10 @@ public class CertSignController {
|
||||
ks.load(jksfile.getInputStream(), password.toCharArray());
|
||||
break;
|
||||
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());
|
||||
|
@ -63,6 +63,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@ -149,23 +150,39 @@ public class GetInfoOnPDF {
|
||||
try {
|
||||
PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata();
|
||||
if (pdMetadata != null) {
|
||||
COSInputStream metaStream = pdMetadata.createInputStream();
|
||||
DomXmpParser domXmpParser = new DomXmpParser();
|
||||
XMPMetadata xmpMeta = domXmpParser.parse(metaStream);
|
||||
try (COSInputStream metaStream = pdMetadata.createInputStream()) {
|
||||
// First try to read raw metadata as string to check for standard keywords
|
||||
byte[] metadataBytes = metaStream.readAllBytes();
|
||||
String rawMetadata = new String(metadataBytes, StandardCharsets.UTF_8);
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
new XmpSerializer().serialize(xmpMeta, baos, true);
|
||||
String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8);
|
||||
if (rawMetadata.contains(standardKeyword)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (xmpString.contains(standardKeyword)) {
|
||||
return true;
|
||||
// If raw check doesn't find it, try parsing with XMP parser
|
||||
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 (
|
||||
Exception
|
||||
e) { // Catching general exception for brevity, ideally you'd catch specific
|
||||
// exceptions.
|
||||
log.error("exception", e);
|
||||
} catch (Exception e) {
|
||||
ExceptionUtils.logException("PDF standard checking", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -391,14 +408,22 @@ public class GetInfoOnPDF {
|
||||
|
||||
if (pdMetadata != null) {
|
||||
try {
|
||||
COSInputStream is = pdMetadata.createInputStream();
|
||||
DomXmpParser domXmpParser = new DomXmpParser();
|
||||
XMPMetadata xmpMeta = domXmpParser.parse(is);
|
||||
try (COSInputStream is = pdMetadata.createInputStream()) {
|
||||
DomXmpParser domXmpParser = new DomXmpParser();
|
||||
XMPMetadata xmpMeta = domXmpParser.parse(is);
|
||||
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
new XmpSerializer().serialize(xmpMeta, os, true);
|
||||
xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8);
|
||||
} catch (XmpParsingException | IOException e) {
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
new XmpSerializer().serialize(xmpMeta, os, true);
|
||||
xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import stirling.software.SPDF.model.api.security.AddPasswordRequest;
|
||||
import stirling.software.SPDF.model.api.security.PDFPasswordRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@ -42,12 +43,19 @@ public class PasswordController {
|
||||
MultipartFile fileInput = request.getFileInput();
|
||||
String password = request.getPassword();
|
||||
PDDocument document = pdfDocumentFactory.load(fileInput, password);
|
||||
document.setAllSecurityToBeRemoved(true);
|
||||
return WebResponseUtils.pdfDocToWebResponse(
|
||||
document,
|
||||
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "")
|
||||
+ "_password_removed.pdf");
|
||||
|
||||
try {
|
||||
document.setAllSecurityToBeRemoved(true);
|
||||
return WebResponseUtils.pdfDocToWebResponse(
|
||||
document,
|
||||
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")
|
||||
|
@ -41,6 +41,7 @@ import stirling.software.SPDF.model.api.security.SignatureValidationRequest;
|
||||
import stirling.software.SPDF.model.api.security.SignatureValidationResult;
|
||||
import stirling.software.SPDF.service.CertificateValidationService;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/security")
|
||||
@ -82,7 +83,12 @@ public class ValidateSignatureController {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
customCert = (X509Certificate) cf.generateCertificate(certStream);
|
||||
} 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,7 @@ import stirling.software.SPDF.service.SignatureService;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.service.UserServiceInterface;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
|
||||
@Controller
|
||||
@ -263,13 +264,17 @@ public class GeneralWebController {
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error processing filename", e);
|
||||
throw ExceptionUtils.createRuntimeException(
|
||||
"error.fontLoadingFailed",
|
||||
"Error processing font file",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,18 @@ public class ProcessPdfWithOcrRequest extends PDFFile {
|
||||
defaultValue = "[\"eng\"]")
|
||||
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(
|
||||
description = "Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
@ -31,4 +43,7 @@ public class ProcessPdfWithOcrRequest extends PDFFile {
|
||||
allowableValues = {"hocr", "sandwich"},
|
||||
defaultValue = "hocr")
|
||||
private String ocrRenderType = "hocr";
|
||||
|
||||
@Schema(description = "Remove images from the output PDF if set to true")
|
||||
private boolean removeImagesAfter;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user