Compare commits

...

136 Commits

Author SHA1 Message Date
Anthony Stirling
50ee829e5f Merge pull request #682 from Stirling-Tools/ebook
Ebook
2024-01-10 00:41:54 +00:00
Anthony Stirling
1924dfb4f1 Merge branch 'main' into ebook 2024-01-10 00:40:17 +00:00
Anthony Stirling
873a4ecb7e revert 2024-01-10 00:39:26 +00:00
Anthony Stirling
32da14acbf log removal 2024-01-10 00:37:55 +00:00
Anthony Stirling
139c793b5e 0.19.1 2024-01-10 00:33:22 +00:00
Anthony Stirling
e717d83f75 fixes and timeouts 2024-01-10 00:33:07 +00:00
Anthony Stirling
ef12c2f892 Add ebook support 2024-01-09 22:39:21 +00:00
Anthony Stirling
362a7ff434 Update releaseArtifacts.yml 2024-01-06 23:06:58 +00:00
Anthony Stirling
624e015315 Update PipelineFeature.md 2024-01-06 22:56:56 +00:00
Anthony Stirling
d76752d7f6 Create PipelineFeature.md 2024-01-06 22:53:07 +00:00
Anthony Stirling
7260e578e3 Merge pull request #666 from Stirling-Tools/stamp
docs
2024-01-06 22:22:34 +00:00
Anthony Stirling
2544b762ba Merge branch 'main' into stamp 2024-01-06 22:18:29 +00:00
Anthony Stirling
164d1abdbb Delete PipelineUsage.md 2024-01-06 22:17:03 +00:00
Anthony Stirling
863b48b5a9 Delete CNAME 2024-01-06 21:09:37 +00:00
Anthony Stirling
b5e0e147ac Merge pull request #651 from PeterDaveHelloKitchen/OptimizeDockerfile
Optimize DockerfileBase for Improved Efficiency and Reduced Size
2024-01-05 20:28:54 +00:00
Anthony Stirling
2fe454ea47 Merge pull request #665 from albanobattistella/patch-7
Update messages_it_IT.properties
2024-01-05 20:23:05 +00:00
albanobattistella
c8458ffe50 Update messages_it_IT.properties 2024-01-05 20:39:01 +01:00
Anthony Stirling
572f9f728f lang 2024-01-05 18:00:30 +00:00
Anthony Stirling
6c963e1b6c format 2024-01-04 23:04:15 +00:00
Anthony Stirling
770b61bb7a Update build.gradle 2024-01-04 21:09:22 +00:00
Anthony Stirling
db64b3f71d lang and footer updates 2024-01-04 20:59:52 +00:00
Anthony Stirling
5fad085db5 Merge pull request #656 from Stirling-Tools/licenses
Licenses
2024-01-04 19:22:22 +00:00
Anthony Stirling
7ed8a69326 test 2024-01-04 19:17:38 +00:00
Anthony Stirling
5d1786cda0 version bump 2024-01-04 19:01:53 +00:00
Anthony Stirling
550f8b0eea licenses remove about 2024-01-04 19:01:37 +00:00
Anthony Stirling
b5d5b6e3e2 remove comment 2024-01-04 18:59:54 +00:00
Anthony Stirling
97b6f0eeb4 Merge remote-tracking branch 'origin/main' into licenses 2024-01-04 18:56:54 +00:00
Anthony Stirling
bd7e2fea0b Merge pull request #650 from Stirling-Tools/cert-sign
fix: pdf certificate signing
2024-01-04 18:54:40 +00:00
Anthony Stirling
eee7e4d707 Merge branch 'main' into cert-sign 2024-01-04 18:50:02 +00:00
Anthony Stirling
43410de851 Merge remote-tracking branch 'origin/main' into licenses 2024-01-04 18:48:05 +00:00
Anthony Stirling
37c92ee9aa Merge remote-tracking branch 'origin/main' into licenses 2024-01-04 18:42:14 +00:00
Anthony Stirling
351cf25f86 license lang 2024-01-04 18:41:33 +00:00
Anthony Stirling
10cb02020c Merge pull request #655 from 92mn/main
Adding Serbian (Latin) Language
2024-01-04 18:30:43 +00:00
92mn
5e40f00bae Update languages.html 2024-01-04 19:16:59 +01:00
92mn
cebc0daf2b Rename message_sr_Latin_RS.propreties to message_sr-Latn-RS.propreties 2024-01-04 19:16:31 +01:00
92mn
04f3f735fc Added Serbian Latin
Added Serbian Latin translation
2024-01-04 17:28:58 +01:00
92mn
f7ef8c32aa Added Serbian flag
Added Serbian flag
2024-01-04 16:33:10 +01:00
92mn
49f2071a93 Update languages.html
Added Serbian ( Latin )
2024-01-04 16:30:00 +01:00
Peter Dave Hello
ecb62e0c94 Apply --no-cache-dir to pip upgrade in DockerfileBase
Aligned pip upgrade command with others by adding the `--no-cache-dir`
flag to reduce image layer size.
2024-01-04 20:56:24 +08:00
Peter Dave Hello
846ebe6dda Refine Tesseract-OCR file backup process in DockerfileBase 2024-01-04 20:55:55 +08:00
Peter Dave Hello
56afd35c82 Refactor DockerfileBase to clean up apt cache after package installation
Changes include:
- Cleaning up the apt cache by adding `rm -rf /var/lib/apt/lists/*`
  after each package installation within the same RUN statement.
- Ensuring the Docker image size is minimized by removing unnecessary
  files immediately after use.

These adjustments will result in a more space-efficient Docker image.
2024-01-04 20:43:46 +08:00
sbplat
eadd513b02 chore(translation): add certsign jks to en-GB 2024-01-03 21:54:15 -05:00
sbplat
97f581ad6d feat: add java keystore certificate option for pdf signing 2024-01-03 21:51:30 -05:00
sbplat
d96a3db60a fix: add pem support for cert sign 2024-01-03 18:21:59 -05:00
Anthony Stirling
0592bac5bf liceneses without translation or GH action 2024-01-03 23:01:33 +00:00
Anthony Stirling
4b0df4ffd5 Update README.md 2024-01-03 21:28:06 +00:00
sbplat
a244d563f2 Merge branch 'main' into cert-sign 2024-01-03 15:50:46 -05:00
sbplat
f433e8032f fix: pkcs12 signing
TODO: add PEM support and use page number
2024-01-03 15:49:08 -05:00
Anthony Stirling
23b85dc47c Merge pull request #647 from albanobattistella/patch-5
Update messages_it_IT.properties
2024-01-03 20:41:16 +00:00
Anthony Stirling
6a9ef7d538 Update releaseArtifacts.yml 2024-01-03 18:51:41 +00:00
Anthony Stirling
c75efede79 Update push-docker.yml 2024-01-03 18:46:29 +00:00
Anthony Stirling
a0212bbfb7 Merge pull request #644 from Antiarchitect/improve-formatting
Make ./gradlew executable; accept .java files improvements
2024-01-03 18:25:56 +00:00
Anthony Stirling
87efa175cb Merge branch 'main' into improve-formatting 2024-01-03 18:13:31 +00:00
Anthony Stirling
ad7150d616 Merge pull request #649 from Stirling-Tools/eol
Eol
2024-01-03 18:11:30 +00:00
Anthony Stirling
6fe268adcb eol 2024-01-03 17:59:04 +00:00
Anthony Stirling
0c2b05eabf Update .gitattributes 2024-01-03 17:58:02 +00:00
albanobattistella
38ebc28108 Update messages_it_IT.properties 2024-01-03 18:29:01 +01:00
Anthony Stirling
0a08831aac Merge pull request #646 from PeterDaveHelloKitchen/zh_TW
Add a basic Traditional Chinese translation
2024-01-03 17:00:45 +00:00
Peter Dave Hello
2a744473f9 Add a basic Traditional Chinese translation
The svg flag file came from:

Flag of the Republic of China - Taiwan - Wikipedia
https://en.wikipedia.org/wiki/Taiwan#/media/File:Flag_of_the_Republic_of_China.svg
2024-01-04 00:01:15 +08:00
Anthony Stirling
56ce53a966 Merge pull request #645 from PeterDaveHelloKitchen/UpdateLanguageInREADME.md
Update supported languages in README to include all 24
2024-01-03 14:38:46 +00:00
Peter Dave Hello
6baf1f94c1 Update supported languages in README to include all 24
Adjusted the README to correctly enumerate all 24 languages supported by
the project. This change includes the addition of Hungarian (Magyar) and
Bulgarian (Български) to the list, based on the properties files located
in 'src/main/resources'.
2024-01-03 21:42:48 +08:00
Andrey Voronkov
8a57165547 Make ./gradlew executable; accept .java files improvements after running ./gradlew build 2024-01-03 03:21:11 +03:00
Anthony Stirling
de9e9a0f84 Merge pull request #642 from mannam11/on_hover-pagenumber-display#527
display page numbers on mouse-hover issue#527
2024-01-02 22:08:44 +00:00
Anthony Stirling
73a55c0666 Merge branch 'main' into on_hover-pagenumber-display#527 2024-01-02 21:53:40 +00:00
Anthony Stirling
4ac5262be2 Update test.yml 2024-01-02 21:52:51 +00:00
Anthony Stirling
dfee149da0 Merge branch 'main' into on_hover-pagenumber-display#527 2024-01-02 21:35:29 +00:00
Anthony Stirling
fbe0a8ddcc Merge pull request #643 from Stirling-Tools/githubActionTest
GitHub action for docker testing
2024-01-02 21:34:58 +00:00
Anthony Stirling
56a1867270 Update test.sh 2024-01-02 21:17:25 +00:00
Anthony Stirling
c23a5ad5fb Merge branch 'main' into githubActionTest 2024-01-02 21:11:31 +00:00
Anthony Stirling
31fbeaae1d Update test.yml 2024-01-02 21:11:05 +00:00
Anthony Stirling
e0d79990c8 Update test.yml 2024-01-02 21:10:11 +00:00
Anthony Stirling
468808167c Update test.yml 2024-01-02 20:54:33 +00:00
mannam
5af5794dfe display page number on mouse-hover issue#527 2024-01-02 16:44:57 +05:30
Anthony Stirling
1d470691a5 Merge pull request #640 from Antiarchitect/add-meta-description
Add meta::description; pass header variable
2024-01-02 00:08:42 +00:00
Andrey Voronkov
f32832f70d Add meta::description; pass header variable 2024-01-02 02:44:49 +03:00
Anthony Stirling
cd0464092a Merge pull request #638 from ThePandaB0i/patch-1
Fixed Turkish languages.html
2024-01-01 21:09:35 +00:00
ThePandaB0i
c67eaf2b4d Fixed Turkish languages.html
Added the missing "data-bs-language-code" to Turkish language dropdown item.
2024-01-01 23:52:03 +03:00
Anthony Stirling
b1f80bc9f6 Update test.yml 2024-01-01 20:07:37 +00:00
Anthony Stirling
f3742ebeb6 Update test.yml 2024-01-01 19:53:21 +00:00
Anthony Stirling
adc7b9606b Update test.yml 2024-01-01 19:53:09 +00:00
Anthony Stirling
aa34257080 Update test.yml 2024-01-01 19:52:26 +00:00
Anthony Stirling
0f126eaf81 Create test.yml 2024-01-01 19:46:33 +00:00
Anthony Stirling
7389543af6 Merge pull request #623 from Stirling-Tools/changes
pipeline Changes and example yml for testing
2024-01-01 19:32:59 +00:00
Anthony Stirling
9795c68220 revert 2024-01-01 19:30:43 +00:00
Anthony Stirling
7ffa447cbc remove logs 2024-01-01 18:57:12 +00:00
Anthony Stirling
d755fd1861 opencsv 2024-01-01 18:31:36 +00:00
Anthony Stirling
e273294360 deps 2024-01-01 18:02:54 +00:00
Anthony Stirling
827ed62761 Merge branch 'main' into changes 2024-01-01 17:06:03 +00:00
Anthony Stirling
ee96d2a0e3 Update dependabot.yml 2024-01-01 16:22:55 +00:00
Anthony Stirling
044a779a7c Test files update 2024-01-01 14:57:52 +00:00
Anthony Stirling
03a8f45128 cleanup 2024-01-01 14:46:19 +00:00
Anthony Stirling
b5423f3434 langs 2024-01-01 14:26:41 +00:00
Anthony Stirling
88c993367f init lang changes 2024-01-01 14:19:22 +00:00
Anthony Stirling
04acdb3b02 Fix for ANY values and settings button enablement 2024-01-01 13:57:22 +00:00
Anthony Stirling
cd3cc15888 minor changes 2024-01-01 13:46:09 +00:00
Anthony Stirling
76e6a23674 split fixes 2024-01-01 12:14:46 +00:00
Anthony Stirling
4fbfd0bae4 print docker logs on fail 2023-12-31 19:13:24 +00:00
Anthony Stirling
b74819cf6c lang 2023-12-31 16:01:27 +00:00
Anthony Stirling
a5ad9e13fe todo 2023-12-31 14:56:08 +00:00
Anthony Stirling
328e873344 remove prints 2023-12-31 13:35:58 +00:00
Anthony Stirling
39045df785 formats 2023-12-31 13:34:35 +00:00
Anthony Stirling
d83bd1ae94 Merge branch 'main' into changes 2023-12-31 13:33:10 +00:00
Anthony Stirling
143b770882 format 2023-12-31 13:28:18 +00:00
Anthony Stirling
d91c600925 pipeline fix 2023-12-31 13:27:06 +00:00
Anthony Stirling
cbac784c57 pipeline 2023-12-31 13:16:04 +00:00
Anthony Stirling
f535387ac4 pipeline enhance for MI 2023-12-31 13:05:38 +00:00
Anthony Stirling
739dcc1327 Merge pull request #621 from Stirling-Tools/sbplat-patch-1
misc: change all repo links from Frooodle to Stirling-Tools
2023-12-31 10:06:37 +00:00
sbplat
3864e130cc misc: change all repo links from Frooodle to Stirling-Tools 2023-12-30 18:36:07 -05:00
sbplat
5b0145fa47 misc: change all repo links from Frooodle to Stirling-Tools 2023-12-30 18:22:46 -05:00
sbplat
7dfeb4bb0f Update footer.html 2023-12-30 18:12:06 -05:00
Anthony Stirling
eda91cc556 tests 2023-12-30 21:32:04 +00:00
sbplat
7572db9bd4 Merge pull request #618 from Frooodle/spotless
refactor: have a newline between annotations
2023-12-30 15:14:01 -05:00
sbplat
9e3b50dff3 Merge branch 'main' into spotless 2023-12-30 14:52:19 -05:00
sbplat
cf640c7e3f refactor: have a newline between annotations 2023-12-30 14:50:59 -05:00
Anthony Stirling
ec770e1008 Merge pull request #611 from Frooodle/spotless
Use spotless in `build.gradle` and apply spotless to java source files
2023-12-30 19:44:48 +00:00
sbplat
15e0048bfc Merge pull request #617 from Frooodle/spotlessFormat
Spotless format
2023-12-30 14:39:36 -05:00
Anthony Stirling
b572a5e4c9 auto run spotlessapply 2023-12-30 19:37:46 +00:00
Anthony Stirling
c55a5657a4 Add .git-blame-ignore-revs to ignore formatting commits in git blame 2023-12-30 19:14:14 +00:00
Anthony Stirling
5f771b7851 formatting 2023-12-30 19:11:27 +00:00
Anthony Stirling
c853465d1d tests 2023-12-30 18:56:07 +00:00
sbplat
b58cbdcb61 Merge branch 'main' into spotless 2023-12-30 13:04:23 -05:00
Anthony Stirling
9e81667ecd Merge pull request #616 from albanobattistella/patch-4
Update messages_it_IT.properties
2023-12-30 17:34:23 +00:00
albanobattistella
a92479b505 Update messages_it_IT.properties 2023-12-30 18:19:51 +01:00
Anthony Stirling
6ca9001fe6 enable status check without apikey 2023-12-30 13:42:24 +00:00
Anthony Stirling
279cfa70f5 Merge pull request #615 from mannam11/added-remove-button-529
added a remove button issue#529
2023-12-30 11:27:18 +00:00
Anthony Stirling
f8ad71aa4e Merge branch 'main' into added-remove-button-529 2023-12-30 11:26:32 +00:00
Anthony Stirling
524d198212 Merge pull request #613 from mannam11/mannam11/file-override-fix-529
overriding previously selected files issue#529
2023-12-30 11:26:23 +00:00
Anthony Stirling
1baf458344 Merge branch 'main' into added-remove-button-529 2023-12-30 11:24:56 +00:00
Anthony Stirling
da50e4d212 Merge branch 'main' into mannam11/file-override-fix-529 2023-12-30 11:24:45 +00:00
mannam
bbd8de0899 added a remove button issue#529 2023-12-30 16:42:41 +05:30
Anthony Stirling
c548aa037e Merge pull request #612 from NeilJared/main
Update messages_es_ES.properties
2023-12-30 09:55:38 +00:00
NeilJared
6a7ed615e3 Merge branch 'Frooodle:main' into main 2023-12-30 09:10:26 +01:00
NeilJared
56cbb4381b Update messages_es_ES.properties 2023-12-30 09:09:27 +01:00
mannam
4612b05199 overriding previously selected files issue#529 2023-12-30 13:32:54 +05:30
sbplat
7b43fca6fc ci: gradle build instead of assemble 2023-12-29 21:59:36 -05:00
sbplat
e3c8af7e54 chore: use spotless googleJavaFormat 2023-12-29 21:34:36 -05:00
301 changed files with 16106 additions and 9234 deletions

2
.git-blame-ignore-revs Normal file
View File

@@ -0,0 +1,2 @@
# Formatting
5f771b785130154ed47952635b7acef371ffe0ec

2
.gitattributes vendored
View File

@@ -1,3 +1,5 @@
* text=auto eol=lf
# Ignore all JavaScript files in a directory # Ignore all JavaScript files in a directory
src/main/resources/static/pdfjs/* linguist-vendored src/main/resources/static/pdfjs/* linguist-vendored
src/main/resources/static/pdfjs/** linguist-vendored src/main/resources/static/pdfjs/** linguist-vendored

View File

@@ -9,3 +9,7 @@ updates:
directory: "/" # Location of package manifests directory: "/" # Location of package manifests
schedule: schedule:
interval: "weekly" interval: "weekly"
- package-ecosystem: "docker"
directory: "/" # Location of Dockerfile
schedule:
interval: "weekly"

34
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: "Build repo"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- uses: gradle/gradle-build-action@v2.4.2
with:
gradle-version: 7.6
arguments: build --no-build-cache

View File

@@ -1,55 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
name: "Build repo"
on:
push:
branches: [ "main" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "main" ]
schedule:
- cron: '15 12 * * 1'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
# - name: Initialize CodeQL
# uses: github/codeql-action/init@v2
# with:
# languages: java
- uses: gradle/gradle-build-action@v2.4.2
with:
gradle-version: 7.6
arguments: assemble --no-build-cache
#- name: Perform CodeQL analysis
# uses: github/codeql-action/analyze@v2

48
.github/workflows/licenses-update.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: License Report Workflow
on:
push:
branches:
- main
paths:
- 'build.gradle'
permissions:
contents: write
pull-requests: write
jobs:
generate-license-report:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'adopt'
- name: Run Gradle Command
run: ./gradlew clean generateLicenseReport
- name: Move and Rename License File
run: |
mv build/reports/dependency-license/index.json src/main/resources/static/3rdPartyLicenses.json
- name: Check for Changes
id: git-check
run: |
git add src/main/resources/static/3rdPartyLicenses.json
git diff --staged --exit-code || echo "changes=true" >> $GITHUB_ENV
- name: Commit and Push Changes
if: env.changes == 'true'
run: |
git config --global user.name 'Stirling-PDF-Bot'
git config --global user.email 'Stirling-PDF-Bot@stirlingtools.com'
git commit -m "Update 3rd Party Licenses"
git push

View File

@@ -6,6 +6,9 @@ on:
branches: branches:
- master - master
- main - main
permissions:
contents: read
packages: write
jobs: jobs:
push: push:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -3,7 +3,9 @@ name: Release Artifacts
on: on:
release: release:
types: [created] types: [created]
permissions:
contents: write
packages: write
jobs: jobs:
push: push:
runs-on: ubuntu-latest runs-on: ubuntu-latest

56
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Docker Compose Tests
on:
pull_request:
paths:
- 'src/**'
- '**.gradle'
- '!src/main/java/resources/messages*'
- 'exampleYmlFiles/**'
- 'Dockerfile'
- 'Dockerfile**'
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Set up Java 17
uses: actions/setup-java@v2
with:
java-version: '17'
distribution: 'adopt'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Run Docker Compose Tests
run: |
chmod +x ./gradlew
- name: Get version number
id: versionNumber
run: echo "::set-output name=versionNumber::$(./gradlew printVersion --quiet | tail -1)"
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ steps.versionNumber.outputs.versionNumber }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Install Docker Compose
run: |
sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
- name: Run Docker Compose Tests
run: |
chmod +x ./test.sh
./test.sh

1
CNAME
View File

@@ -1 +0,0 @@
stirlingtools.com

View File

@@ -6,7 +6,8 @@ FROM ubuntu:latest AS base
# JDK for app # JDK for app
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
openjdk-17-jre openjdk-17-jre && \
rm -rf /var/lib/apt/lists/*
# Doc conversion # Doc conversion
RUN apt-get update && \ RUN apt-get update && \
@@ -18,7 +19,8 @@ RUN apt-get update && \
libreoffice-impress \ libreoffice-impress \
python3-uno \ python3-uno \
curl \ curl \
unoconv unoconv && \
rm -rf /var/lib/apt/lists/*
# OCR MY PDF (unpaper for descew and other advanced featues) # OCR MY PDF (unpaper for descew and other advanced featues)
@@ -30,21 +32,12 @@ apt-get update && \
python3-pip \ python3-pip \
ocrmypdf \ ocrmypdf \
unpaper && \ unpaper && \
pip install --upgrade pip && \ rm -rf /var/lib/apt/lists/* && \
mv /usr/share/tesseract-ocr /usr/share/tesseract-ocr-original && \
pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir --upgrade ocrmypdf && \ pip install --no-cache-dir --upgrade ocrmypdf && \
pip install --no-cache-dir --upgrade pillow==10.0.1 reportlab==3.6.13 wheel==0.38.1 setuptools==65.5.1 pyjwt==2.4.0 cryptography==39.0.1 pip install --no-cache-dir --upgrade pillow==10.0.1 reportlab==3.6.13 wheel==0.38.1 setuptools==65.5.1 pyjwt==2.4.0 cryptography==39.0.1
#CV and HTML #CV and HTML
RUN pip install --no-cache-dir opencv-python-headless WeasyPrint RUN pip install --no-cache-dir opencv-python-headless WeasyPrint
# cleanup and etc
RUN rm -rf /var/lib/apt/lists/* && \
mkdir /usr/share/tesseract-ocr-original && \
cp -r /usr/share/tesseract-ocr/* /usr/share/tesseract-ocr-original && \
rm -rf /usr/share/tesseract-ocr

41
FolderScanning.md Normal file
View File

@@ -0,0 +1,41 @@
## User Guide for Local Directory Scanning and File Processing
### Whilst Pipelines are in alpha...
You must enable this alpha functionality by setting
```
system:
enableAlphaFunctionality: true
```
To true like in the above for your `/config/settings.yml` file, after restarting Stirling-PDF you should see both UI and folder scanning enabled.
### Setting Up Watched Folders:
- Create a folder where you want your files to be monitored. This is your 'watched folder'.
- The default directory for this is `./pipeline/watchedFolders/`
- Place any directories you want to be scanned into this folder, this folder should contain multiple folders each for their own tasks and pipelines.
### Configuring Processing with JSON Files:
- In each directory you want processed (e.g `./pipeline/watchedFolders/officePrinter`), include a JSON configuration file.
- This JSON file should specify how you want the files in the directory to be handled (e.g., what operations to perform on them) which can be made, configured and downloaded from Stirling-PDF Pipeline interface.r
### Automatic Scanning and Processing:
- The system automatically checks the watched folder every minute for new directories and files to process.
- When a directory with a valid JSON configuration file is found, it begins processing the files inside as per the configuration.
### Processing Steps:
- Files in each directory are processed according to the instructions in the JSON file.
- This might involve file conversions, data filtering, renaming files, etc. If the output of a step is a zip, this zip will be automatically unzipped as it passes to next process.
### Results and Output:
- After processing, the results are saved in a specified output location. This could be a different folder or location as defined in the JSON file or the default location `./pipeline/finishedFolders/`.
- Each processed file is named and organized according to the rules set in the JSON configuration.
### Completion and Cleanup:
- Once processing is complete, the original files in the watched folder's directory are removed.
- You can find the processed files in the designated output location.
### Error Handling:
- If there's an error during processing, the system will not delete the original files, allowing you to check and retry if necessary.
### User Interaction:
- As a user, your main tasks are to set up the watched folders, place directories with files for processing, and create the corresponding JSON configuration files.
- The system handles the rest, including scanning, processing, and outputting results.

View File

@@ -1,4 +1,4 @@
<p align="center"><img src="https://raw.githubusercontent.com/Frooodle/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1> <p align="center"><img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1>
</p> </p>
@@ -8,9 +8,9 @@ Fork Stirling-PDF and make a new branch out of Main
Then add reference to the language in the navbar by adding a new language entry to the dropdown Then add reference to the language in the navbar by adding a new language entry to the dropdown
https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html
and add a flag svg file to and add a flag svg file to
https://github.com/Frooodle/Stirling-PDF/tree/main/src/main/resources/static/images/flags https://github.com/Stirling-Tools/Stirling-PDF/tree/main/src/main/resources/static/images/flags
Any SVG flags are fine, i got most of mine from [here](https://flagicons.lipis.dev/) Any SVG flags are fine, i got most of mine from [here](https://flagicons.lipis.dev/)
If your language isnt represented by a flag just find whichever closely matches it, such as for Arabic i chose Saudi Arabia If your language isnt represented by a flag just find whichever closely matches it, such as for Arabic i chose Saudi Arabia
@@ -25,7 +25,7 @@ The data-language-code is the code used to reference the file in the next step.
Start by copying the existing english property file Start by copying the existing english property file
[https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties](https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties) [https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties)
Copy and rename it to messages_{your data-language-code here}.properties, in the polish example you would set the name to messages_pl_PL.properties Copy and rename it to messages_{your data-language-code here}.properties, in the polish example you would set the name to messages_pl_PL.properties

View File

@@ -109,7 +109,7 @@ pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
```bash ```bash
cd ~/.git &&\ cd ~/.git &&\
git clone https://github.com/Frooodle/Stirling-PDF.git &&\ git clone https://github.com/Stirling-Tools/Stirling-PDF.git &&\
cd Stirling-PDF &&\ cd Stirling-PDF &&\
chmod +x ./gradlew &&\ chmod +x ./gradlew &&\
./gradlew build ./gradlew build

42
PipelineFeature.md Normal file
View File

@@ -0,0 +1,42 @@
# Pipeline Configuration and Usage Tutorial
## Whilst Pipelines are in alpha...
You must enable this alpha functionality by setting
```
system:
enableAlphaFunctionality: true
```
To true like in the above for your `/config/settings.yml` file, after restarting Stirling-PDF you should see both UI and folder scanning enabled.
## Steps to Configure and Use Your Pipeline
1. **Access Configuration**
- Upon entering the screen, click on the **Configure** button.
2. **Enter Pipeline Name**
- Provide a name for your pipeline in the designated field.
3. **Select Operations**
- Choose the operations for your pipeline (e.g., **Split Pages**), then click **Add Operation**.
4. **Configure Operation Settings**
- Input the necessary settings for each added operation. Settings are highlighted in yellow if customization is needed.
5. **Add More Operations**
- You can add and adjust the order of multiple operations. Ensure each operation's settings are customized.
6. **Save Settings**
- Click **Save Operation Settings** after customizing settings for each operation.
7. **Validate Pipeline**
- Use the **Validation** button to check your pipeline. A green indicator signifies correct setup; a pop-out error indicates issues.
8. **Download Pipeline Configuration**
- To use the configuration for folder scanning (or save it for future use and reupload it), you can also download a JSON file in this menu. You can also pre-load this for future use by placing it in ``/pipeline/defaultWebUIConfigs/``. It will then appear in the dropdown menu for all users to use.
9. **Submit Files for Processing**
- If your pipeline is correctly set up close the configure menu, input the files and hit **Submit**.
10. **Note on Web UI Limitations**
- The current web UI version does not support operations that require multiple different types of inputs, such as adding a separate image to a PDF.

View File

@@ -1,14 +1,14 @@
<p align="center"><img src="https://raw.githubusercontent.com/Frooodle/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1> <p align="center"><img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1>
</p> </p>
[![Docker Pulls](https://img.shields.io/docker/pulls/frooodle/s-pdf)](https://hub.docker.com/r/frooodle/s-pdf) [![Docker Pulls](https://img.shields.io/docker/pulls/frooodle/s-pdf)](https://hub.docker.com/r/frooodle/s-pdf)
[![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/Cn8pWhQRxZ) [![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/Cn8pWhQRxZ)
[![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Frooodle/Stirling-PDF/) [![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/)
[![GitHub Repo stars](https://img.shields.io/github/stars/frooodle/stirling-pdf?style=social)](https://github.com/Frooodle/stirling-pdf) [![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](https://github.com/Stirling-Tools/stirling-pdf)
[![Paypal Donate](https://img.shields.io/badge/Paypal%20Donate-yellow?style=flat&logo=paypal)](https://www.paypal.com/paypalme/froodleplex) [![Paypal Donate](https://img.shields.io/badge/Paypal%20Donate-yellow?style=flat&logo=paypal)](https://www.paypal.com/paypalme/froodleplex)
[![Github Sponser](https://img.shields.io/badge/Github%20Sponsor-yellow?style=flat&logo=github)](https://github.com/sponsors/Frooodle) [![Github Sponser](https://img.shields.io/badge/Github%20Sponsor-yellow?style=flat&logo=github)](https://github.com/sponsors/Frooodle)
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Frooodle/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af) [![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
This is a powerful locally hosted web based PDF manipulation tool using docker that allows you to perform various operations on PDF files, such as splitting merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application started as a 100% ChatGPT-made application and has evolved to include a wide range of features to handle all your PDF needs. This is a powerful locally hosted web based PDF manipulation tool using docker that allows you to perform various operations on PDF files, such as splitting merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application started as a 100% ChatGPT-made application and has evolved to include a wide range of features to handle all your PDF needs.
@@ -22,10 +22,10 @@ Please feel free to submit feature requests or report bugs either through GitHub
## Features ## Features
- Dark mode support. - Dark mode support.
- Custom download options (see [here](https://github.com/Frooodle/Stirling-PDF/blob/main/images/settings.png) for example) - Custom download options (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/images/settings.png) for example)
- Parallel file processing and downloads - Parallel file processing and downloads
- API for integration with external scripts - API for integration with external scripts
- Optional Login and Authentication support (see [here](https://github.com/Frooodle/Stirling-PDF/tree/main#login-authentication) for documentation) - Optional Login and Authentication support (see [here](https://github.com/Stirling-Tools/Stirling-PDF/tree/main#login-authentication) for documentation)
## **PDF Features** ## **PDF Features**
@@ -80,29 +80,29 @@ Please feel free to submit feature requests or report bugs either through GitHub
- Get all information on a PDF to view or export as JSON. - Get all information on a PDF to view or export as JSON.
For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Frooodle/Stirling-PDF/blob/main/Endpoint-groups.md) For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) hosted by the team at adminforge.de Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) hosted by the team at adminforge.de
## Technologies used ## Technologies used
- Spring Boot + Thymeleaf - Spring Boot + Thymeleaf
- PDFBox - [PDFBox](https://github.com/apache/pdfbox/tree/trunk)
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions - [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
- [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF) - [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF)
- HTML, CSS, JavaScript - HTML, CSS, JavaScript
- Docker - Docker
- PDF.js - [PDF.js](https://github.com/mozilla/pdf.js)
- PDF-LIB.js - [PDF-LIB.js](https://github.com/Hopding/pdf-lib)
## How to use ## How to use
### Locally ### Locally
Please view https://github.com/Frooodle/Stirling-PDF/blob/main/LocalRunGuide.md Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LocalRunGuide.md
### Docker / Podman ### Docker / Podman
https://hub.docker.com/r/frooodle/s-pdf https://hub.docker.com/r/frooodle/s-pdf
Stirling PDF has 3 different versions, a Full version, Lite, and ultra-Lite. Depending on the types of features you use you may want a smaller image to save on space. Stirling PDF has 3 different versions, a Full version, Lite, and ultra-Lite. Depending on the types of features you use you may want a smaller image to save on space.
To see what the different versions offer please look at our [version mapping](https://github.com/Frooodle/Stirling-PDF/blob/main/Version-groups.md) To see what the different versions offer please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md)
For people that don't mind about space optimization just use the latest tag. For people that don't mind about space optimization just use the latest tag.
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest?label=Stirling-PDF%20Full) ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest?label=Stirling-PDF%20Full)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest-lite?label=Stirling-PDF%20Lite) ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest-lite?label=Stirling-PDF%20Lite)
@@ -144,17 +144,18 @@ services:
Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "podman". Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "podman".
## Enable OCR/Compression feature ## Enable OCR/Compression feature
Please view https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md
## Want to add your own language? ## Want to add your own language?
Stirling PDF currently supports 21! Stirling PDF currently supports 26!
- English (English) (en_GB) - English (English) (en_GB)
- English (US) (en_US) - English (US) (en_US)
- Arabic (العربية) (ar_AR) - Arabic (العربية) (ar_AR)
- German (Deutsch) (de_DE) - German (Deutsch) (de_DE)
- French (Français) (fr_FR) - French (Français) (fr_FR)
- Spanish (Español) (es_ES) - Spanish (Español) (es_ES)
- Chinese (简体中文) (zh_CN) - Simplified Chinese (简体中文) (zh_CN)
- Traditional Chinese (繁體中文) (zh_TW)
- Catalan (Català) (ca_CA) - Catalan (Català) (ca_CA)
- Italian (Italiano) (it_IT) - Italian (Italiano) (it_IT)
- Swedish (Svenska) (sv_SE) - Swedish (Svenska) (sv_SE)
@@ -170,9 +171,12 @@ Stirling PDF currently supports 21!
- Turkish (Türkçe) (tr_TR) - Turkish (Türkçe) (tr_TR)
- Indonesia (Bahasa Indonesia) (id_ID) - Indonesia (Bahasa Indonesia) (id_ID)
- Hindi (हिंदी) (hi_IN) - Hindi (हिंदी) (hi_IN)
- Hungarian (Magyar) (hu_HU)
- Bulgarian (Български) (bg_BG)
- Sebian Latin alphabet (Srpski) (sr-Latn-RS)
If you want to add your own language to Stirling-PDF please refer If you want to add your own language to Stirling-PDF please refer
https://github.com/Frooodle/Stirling-PDF/blob/main/HowToAddNewLanguage.md https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md
And please create a PR to merge it back in so others can use it! And please create a PR to merge it back in so others can use it!
@@ -224,7 +228,7 @@ metrics:
enabled: true # 'true' to enable Info APIs endpoints (view http://localhost:8080/swagger-ui/index.html#/API to learn more), 'false' to disable enabled: true # 'true' to enable Info APIs endpoints (view http://localhost:8080/swagger-ui/index.html#/API to learn more), 'false' to disable
``` ```
### Extra notes ### Extra notes
- Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Frooodle/Stirling-PDF/blob/main/Endpoint-groups.md) - Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
- customStaticFilePath. Customise static files such as the app logo by placing files in the /customFiles/static/ directory. An example of customising app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF - customStaticFilePath. Customise static files such as the app logo by placing files in the /customFiles/static/ directory. An example of customising app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF
### Environment only parameters ### Environment only parameters
@@ -234,7 +238,7 @@ metrics:
## API ## API
For those wanting to use Stirling-PDFs backend API to link with their own custom scripting to edit PDFs you can view all existing API documentation For those wanting to use Stirling-PDFs backend API to link with their own custom scripting to edit PDFs you can view all existing API documentation
[here](https://app.swaggerhub.com/apis-docs/Frooodle/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF) [here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF)
## Login authentication ## Login authentication

View File

@@ -1,18 +1,28 @@
plugins { plugins {
id 'java' id 'java'
id 'org.springframework.boot' version '3.1.2' id 'org.springframework.boot' version '3.2.1'
id 'io.spring.dependency-management' version '1.1.3' id 'io.spring.dependency-management' version '1.1.3'
id 'org.springdoc.openapi-gradle-plugin' version '1.8.0' id 'org.springdoc.openapi-gradle-plugin' version '1.8.0'
id "io.swagger.swaggerhub" version "1.3.2" id "io.swagger.swaggerhub" version "1.3.2"
id 'edu.sc.seis.launch4j' version '3.0.5' id 'edu.sc.seis.launch4j' version '3.0.5'
id 'com.diffplug.spotless' version '6.23.3'
id 'com.github.jk1.dependency-license-report' version '2.5'
} }
import com.github.jk1.license.render.*
group = 'stirling.software' group = 'stirling.software'
version = '0.18.1' version = '0.19.1'
sourceCompatibility = '17' sourceCompatibility = '17'
repositories { repositories {
mavenCentral() mavenCentral()
}
licenseReport {
renderers = [new JsonReportRenderer()]
} }
sourceSets { sourceSets {
@@ -61,21 +71,37 @@ launch4j {
messagesInstanceAlreadyExists="Stirling-PDF is already running." messagesInstanceAlreadyExists="Stirling-PDF is already running."
} }
spotless {
java {
target project.fileTree('src/main/java')
googleJavaFormat('1.19.1').aosp().reorderImports(false)
importOrder('java', 'javax', 'org', 'com', 'net', 'io')
toggleOffOn()
trimTrailingWhitespace()
indentWithSpaces()
endWithNewline()
}
}
dependencies { dependencies {
//security updates //security updates
implementation 'ch.qos.logback:logback-classic:1.4.14' implementation 'ch.qos.logback:logback-classic:1.4.14'
implementation 'ch.qos.logback:logback-core:1.4.14' implementation 'ch.qos.logback:logback-core:1.4.14'
implementation 'org.springframework:spring-webmvc:6.0.15' implementation 'org.springframework:spring-webmvc:6.1.2'
implementation 'org.yaml:snakeyaml:2.1' implementation 'org.yaml:snakeyaml:2.2'
implementation 'org.springframework.boot:spring-boot-starter-web:3.2.1' implementation 'org.springframework.boot:spring-boot-starter-web:3.2.1'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.2.1' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.2.1'
if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') { if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') {
implementation 'org.springframework.boot:spring-boot-starter-security:3.2.1' implementation 'org.springframework.boot:spring-boot-starter-security:3.2.1'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE'
implementation "org.springframework.boot:spring-boot-starter-data-jpa" implementation "org.springframework.boot:spring-boot-starter-data-jpa:3.2.1"
implementation "com.h2database:h2"
//2.2.x requires rebuild of DB file.. need migration path
implementation "com.h2database:h2:2.1.214"
} }
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.2.1' testImplementation 'org.springframework.boot:spring-boot-starter-test:3.2.1'
@@ -107,15 +133,15 @@ dependencies {
//general PDF //general PDF
// https://mvnrepository.com/artifact/com.opencsv/opencsv // https://mvnrepository.com/artifact/com.opencsv/opencsv
implementation ('com.opencsv:opencsv:5.7.1') { implementation ('com.opencsv:opencsv:5.9') {
exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'commons-logging', module: 'commons-logging'
} }
implementation ('org.apache.pdfbox:pdfbox:2.0.29'){ implementation ('org.apache.pdfbox:pdfbox:2.0.30'){
exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'commons-logging', module: 'commons-logging'
} }
implementation ('org.apache.pdfbox:xmpbox:2.0.29'){ implementation ('org.apache.pdfbox:xmpbox:2.0.30'){
exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'commons-logging', module: 'commons-logging'
} }
@@ -134,6 +160,9 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok:1.18.28' annotationProcessor 'org.projectlombok:lombok:1.18.28'
} }
tasks.withType(JavaCompile) {
dependsOn 'spotlessApply'
}
task writeVersion { task writeVersion {
def propsFile = file('src/main/resources/version.properties') def propsFile = file('src/main/resources/version.properties')
@@ -164,7 +193,7 @@ jar {
} }
tasks.named('test') { tasks.named('test') {
useJUnitPlatform() useJUnitPlatform()
} }
task printVersion { task printVersion {

View File

@@ -1,15 +1,15 @@
apiVersion: v2 apiVersion: v2
appVersion: 0.14.2 appVersion: 0.14.2
description: locally hosted web application that allows you to perform various operations on PDF files description: locally hosted web application that allows you to perform various operations on PDF files
home: https://github.com/Frooodle/Stirling-PDF home: https://github.com/Stirling-Tools/Stirling-PDF
keywords: keywords:
- stirling-pdf - stirling-pdf
- helm - helm
- charts repo - charts repo
maintainers: maintainers:
- name: Frooodle - name: Stirling-Tools
url: https://github.com/Frooodle/Stirling-PDF url: https://github.com/Stirling-Tools/Stirling-PDF
name: stirling-pdf-chart name: stirling-pdf-chart
sources: sources:
- https://github.com/Frooodle/Stirling-PDF - https://github.com/Stirling-Tools/Stirling-PDF
version: 1.0.0 version: 1.0.0

View File

@@ -0,0 +1,31 @@
version: '3.3'
services:
stirling-pdf:
container_name: Stirling-PDF-Lite-Security
image: frooodle/s-pdf:latest-lite
deploy:
resources:
limits:
memory: 2G
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/tesseract-ocr/5/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true"
SYSTEM_DEFAULTLOCALE: en_US
UI_APPNAME: Stirling-PDF-Lite
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Lite Latest with Security
UI_APPNAMENAVBAR: Stirling-PDF-Lite Latest
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
restart: on-failure:5

View File

@@ -0,0 +1,30 @@
version: '3.3'
services:
stirling-pdf:
container_name: Stirling-PDF-Lite
image: frooodle/s-pdf:latest-lite
deploy:
resources:
limits:
memory: 2G
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"]
interval: 5s
timeout: 10s
retries: 16
ports:
- 8080:8080
volumes:
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "false"
SECURITY_ENABLELOGIN: "false"
SYSTEM_DEFAULTLOCALE: en_US
UI_APPNAME: Stirling-PDF-Lite
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Lite Latest
UI_APPNAMENAVBAR: Stirling-PDF-Lite Latest
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
restart: on-failure:5

View File

@@ -0,0 +1,31 @@
version: '3.3'
services:
stirling-pdf:
container_name: Stirling-PDF-Security
image: frooodle/s-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/tesseract-ocr/5/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true"
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"
restart: on-failure:5

View File

@@ -0,0 +1,31 @@
version: '3.3'
services:
stirling-pdf:
container_name: Stirling-PDF-Ultra-Lite-Security
image: frooodle/s-pdf:latest-ultra-lite
deploy:
resources:
limits:
memory: 1G
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/tesseract-ocr/5/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true"
SYSTEM_DEFAULTLOCALE: en_US
UI_APPNAME: Stirling-PDF-Lite
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Lite Latest with Security
UI_APPNAMENAVBAR: Stirling-PDF-Lite Latest
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
restart: on-failure:5

View File

@@ -0,0 +1,30 @@
version: '3.3'
services:
stirling-pdf:
container_name: Stirling-PDF-Ultra-Lite
image: frooodle/s-pdf:latest-ultra-lite
deploy:
resources:
limits:
memory: 1G
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"]
interval: 5s
timeout: 10s
retries: 16
ports:
- 8080:8080
volumes:
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "false"
SECURITY_ENABLELOGIN: "false"
SYSTEM_DEFAULTLOCALE: en_US
UI_APPNAME: Stirling-PDF-Ultra-lite
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Ultra-lite Latest
UI_APPNAMENAVBAR: Stirling-PDF-Ultra-lite Latest
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
restart: on-failure:5

View File

@@ -0,0 +1,31 @@
version: '3.3'
services:
stirling-pdf:
container_name: Stirling-PDF
image: frooodle/s-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 -qv 'Please sign in'"]
interval: 5s
timeout: 10s
retries: 16
ports:
- 8080:8080
volumes:
- /stirling/latest/data:/usr/share/tesseract-ocr/5/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "false"
SECURITY_ENABLELOGIN: "false"
SYSTEM_DEFAULTLOCALE: en_US
UI_APPNAME: Stirling-PDF
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest
UI_APPNAMENAVBAR: Stirling-PDF Latest
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
restart: on-failure:5

0
gradlew vendored Normal file → Executable file
View File

View File

@@ -2,13 +2,13 @@ echo "Running Stirling PDF with DOCKER_ENABLE_SECURITY=${DOCKER_ENABLE_SECURITY}
# Check for DOCKER_ENABLE_SECURITY and download the appropriate JAR if required # Check for DOCKER_ENABLE_SECURITY and download the appropriate JAR if required
if [ "$DOCKER_ENABLE_SECURITY" = "true" ] && [ "$VERSION_TAG" != "alpha" ]; then if [ "$DOCKER_ENABLE_SECURITY" = "true" ] && [ "$VERSION_TAG" != "alpha" ]; then
if [ ! -f app-security.jar ]; then if [ ! -f app-security.jar ]; then
echo "Trying to download from: https://github.com/Frooodle/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar" echo "Trying to download from: https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar"
curl -L -o app-security.jar https://github.com/Frooodle/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar curl -L -o app-security.jar https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar
# If the first download attempt failed, try with the 'v' prefix # If the first download attempt failed, try with the 'v' prefix
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "Trying to download from: https://github.com/Frooodle/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar" echo "Trying to download from: https://github.com/Stirling-Tools/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar"
curl -L -o app-security.jar https://github.com/Frooodle/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar curl -L -o app-security.jar https://github.com/Stirling-Tools/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar
fi fi
if [ $? -eq 0 ]; then # checks if curl was successful if [ $? -eq 0 ]; then # checks if curl was successful
@@ -16,4 +16,4 @@ if [ "$DOCKER_ENABLE_SECURITY" = "true" ] && [ "$VERSION_TAG" != "alpha" ]; then
ln -s app-security.jar app.jar ln -s app-security.jar app.jar
fi fi
fi fi
fi fi

View File

@@ -0,0 +1,64 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pdfbox.examples.signature;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSTypedData;
/**
* Wraps a InputStream into a CMSProcessable object for bouncy castle. It's a memory saving
* alternative to the {@link org.bouncycastle.cms.CMSProcessableByteArray CMSProcessableByteArray}
* class.
*
* @author Thomas Chojecki
*/
class CMSProcessableInputStream implements CMSTypedData {
private final InputStream in;
private final ASN1ObjectIdentifier contentType;
CMSProcessableInputStream(InputStream is) {
this(new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId()), is);
}
CMSProcessableInputStream(ASN1ObjectIdentifier type, InputStream is) {
contentType = type;
in = is;
}
@Override
public Object getContent() {
return in;
}
@Override
public void write(OutputStream out) throws IOException, CMSException {
// read the content only one time
in.transferTo(out);
in.close();
}
@Override
public ASN1ObjectIdentifier getContentType() {
return contentType;
}
}

View File

@@ -0,0 +1,170 @@
/*
* Copyright 2015 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pdfbox.examples.signature;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Enumeration;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
public abstract class CreateSignatureBase implements SignatureInterface {
private PrivateKey privateKey;
private Certificate[] certificateChain;
private String tsaUrl;
private boolean externalSigning;
/**
* Initialize the signature creator with a keystore (pkcs12) and pin that should be used for the
* signature.
*
* @param keystore is a pkcs12 keystore.
* @param pin is the pin for the keystore / private key
* @throws KeyStoreException if the keystore has not been initialized (loaded)
* @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found
* @throws UnrecoverableKeyException if the given password is wrong
* @throws CertificateException if the certificate is not valid as signing time
* @throws IOException if no certificate could be found
*/
public CreateSignatureBase(KeyStore keystore, char[] pin)
throws KeyStoreException,
UnrecoverableKeyException,
NoSuchAlgorithmException,
IOException,
CertificateException {
// grabs the first alias from the keystore and get the private key. An
// alternative method or constructor could be used for setting a specific
// alias that should be used.
Enumeration<String> aliases = keystore.aliases();
String alias;
Certificate cert = null;
while (cert == null && aliases.hasMoreElements()) {
alias = aliases.nextElement();
setPrivateKey((PrivateKey) keystore.getKey(alias, pin));
Certificate[] certChain = keystore.getCertificateChain(alias);
if (certChain != null) {
setCertificateChain(certChain);
cert = certChain[0];
if (cert instanceof X509Certificate) {
// avoid expired certificate
((X509Certificate) cert).checkValidity();
//// SigUtils.checkCertificateUsage((X509Certificate) cert);
}
}
}
if (cert == null) {
throw new IOException("Could not find certificate");
}
}
public final void setPrivateKey(PrivateKey privateKey) {
this.privateKey = privateKey;
}
public final void setCertificateChain(final Certificate[] certificateChain) {
this.certificateChain = certificateChain;
}
public Certificate[] getCertificateChain() {
return certificateChain;
}
public void setTsaUrl(String tsaUrl) {
this.tsaUrl = tsaUrl;
}
/**
* SignatureInterface sample implementation.
*
* <p>This method will be called from inside of the pdfbox and create the PKCS #7 signature. The
* given InputStream contains the bytes that are given by the byte range.
*
* <p>This method is for internal use only.
*
* <p>Use your favorite cryptographic library to implement PKCS #7 signature creation. If you
* want to create the hash and the signature separately (e.g. to transfer only the hash to an
* external application), read <a href="https://stackoverflow.com/questions/41767351">this
* answer</a> or <a href="https://stackoverflow.com/questions/56867465">this answer</a>.
*
* @throws IOException
*/
@Override
public byte[] sign(InputStream content) throws IOException {
// cannot be done private (interface)
try {
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
X509Certificate cert = (X509Certificate) certificateChain[0];
ContentSigner sha1Signer =
new JcaContentSignerBuilder("SHA256WithRSA").build(privateKey);
gen.addSignerInfoGenerator(
new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().build())
.build(sha1Signer, cert));
gen.addCertificates(new JcaCertStore(Arrays.asList(certificateChain)));
CMSProcessableInputStream msg = new CMSProcessableInputStream(content);
CMSSignedData signedData = gen.generate(msg, false);
if (tsaUrl != null && !tsaUrl.isEmpty()) {
ValidationTimeStamp validation = new ValidationTimeStamp(tsaUrl);
signedData = validation.addSignedTimeStamp(signedData);
}
return signedData.getEncoded();
} catch (GeneralSecurityException
| CMSException
| OperatorCreationException
| URISyntaxException e) {
throw new IOException(e);
}
}
/**
* Set if external signing scenario should be used. If {@code false}, SignatureInterface would
* be used for signing.
*
* <p>Default: {@code false}
*
* @param externalSigning {@code true} if external signing should be performed
*/
public void setExternalSigning(boolean externalSigning) {
this.externalSigning = externalSigning;
}
public boolean isExternalSigning() {
return externalSigning;
}
}

View File

@@ -0,0 +1,176 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pdfbox.examples.signature;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Random;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
import org.bouncycastle.operator.DigestAlgorithmIdentifierFinder;
import org.bouncycastle.tsp.TSPException;
import org.bouncycastle.tsp.TimeStampRequest;
import org.bouncycastle.tsp.TimeStampRequestGenerator;
import org.bouncycastle.tsp.TimeStampResponse;
import org.bouncycastle.tsp.TimeStampToken;
/**
* Time Stamping Authority (TSA) Client [RFC 3161].
*
* @author Vakhtang Koroghlishvili
* @author John Hewson
*/
public class TSAClient {
private static final Logger LOG = LogManager.getLogger(TSAClient.class);
private static final DigestAlgorithmIdentifierFinder ALGORITHM_OID_FINDER =
new DefaultDigestAlgorithmIdentifierFinder();
private final URL url;
private final String username;
private final String password;
private final MessageDigest digest;
// SecureRandom.getInstanceStrong() would be better, but sometimes blocks on Linux
private static final Random RANDOM = new SecureRandom();
/**
* @param url the URL of the TSA service
* @param username user name of TSA
* @param password password of TSA
* @param digest the message digest to use
*/
public TSAClient(URL url, String username, String password, MessageDigest digest) {
this.url = url;
this.username = username;
this.password = password;
this.digest = digest;
}
/**
* @param content
* @return the time stamp token
* @throws IOException if there was an error with the connection or data from the TSA server, or
* if the time stamp response could not be validated
*/
public TimeStampToken getTimeStampToken(InputStream content) throws IOException {
digest.reset();
DigestInputStream dis = new DigestInputStream(content, digest);
while (dis.read() != -1) {
// do nothing
}
byte[] hash = digest.digest();
// 32-bit cryptographic nonce
int nonce = RANDOM.nextInt();
// generate TSA request
TimeStampRequestGenerator tsaGenerator = new TimeStampRequestGenerator();
tsaGenerator.setCertReq(true);
ASN1ObjectIdentifier oid = ALGORITHM_OID_FINDER.find(digest.getAlgorithm()).getAlgorithm();
TimeStampRequest request = tsaGenerator.generate(oid, hash, BigInteger.valueOf(nonce));
// get TSA response
byte[] tsaResponse = getTSAResponse(request.getEncoded());
TimeStampResponse response;
try {
response = new TimeStampResponse(tsaResponse);
response.validate(request);
} catch (TSPException e) {
throw new IOException(e);
}
TimeStampToken timeStampToken = response.getTimeStampToken();
if (timeStampToken == null) {
// https://www.ietf.org/rfc/rfc3161.html#section-2.4.2
throw new IOException(
"Response from "
+ url
+ " does not have a time stamp token, status: "
+ response.getStatus()
+ " ("
+ response.getStatusString()
+ ")");
}
return timeStampToken;
}
// gets response data for the given encoded TimeStampRequest data
// throws IOException if a connection to the TSA cannot be established
private byte[] getTSAResponse(byte[] request) throws IOException {
LOG.debug("Opening connection to TSA server");
// todo: support proxy servers
URLConnection connection = url.openConnection();
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setRequestProperty("Content-Type", "application/timestamp-query");
LOG.debug("Established connection to TSA server");
if (username != null && password != null && !username.isEmpty() && !password.isEmpty()) {
String contentEncoding = connection.getContentEncoding();
if (contentEncoding == null) {
contentEncoding = StandardCharsets.UTF_8.name();
}
connection.setRequestProperty(
"Authorization",
"Basic "
+ new String(
Base64.getEncoder()
.encode(
(username + ":" + password)
.getBytes(contentEncoding))));
}
// read response
try (OutputStream output = connection.getOutputStream()) {
output.write(request);
} catch (IOException ex) {
LOG.error("Exception when writing to {}", this.url, ex);
throw ex;
}
LOG.debug("Waiting for response from TSA server");
byte[] response;
try (InputStream input = connection.getInputStream()) {
response = input.readAllBytes();
} catch (IOException ex) {
LOG.error("Exception when reading from {}", this.url, ex);
throw ex;
}
LOG.debug("Received response from TSA server");
return response;
}
}

View File

@@ -0,0 +1,134 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pdfbox.examples.signature;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.DERSet;
import org.bouncycastle.asn1.cms.Attribute;
import org.bouncycastle.asn1.cms.AttributeTable;
import org.bouncycastle.asn1.cms.Attributes;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationStore;
import org.bouncycastle.tsp.TimeStampToken;
/**
* This class wraps the TSAClient and the work that has to be done with it. Like Adding Signed
* TimeStamps to a signature, or creating a CMS timestamp attribute (with a signed timestamp)
*
* @author Others
* @author Alexis Suter
*/
public class ValidationTimeStamp {
private TSAClient tsaClient;
/**
* @param tsaUrl The url where TS-Request will be done.
* @throws NoSuchAlgorithmException
* @throws MalformedURLException
* @throws java.net.URISyntaxException
*/
public ValidationTimeStamp(String tsaUrl)
throws NoSuchAlgorithmException, MalformedURLException, URISyntaxException {
if (tsaUrl != null) {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
this.tsaClient = new TSAClient(new URI(tsaUrl).toURL(), null, null, digest);
}
}
/**
* Creates a signed timestamp token by the given input stream.
*
* @param content InputStream of the content to sign
* @return the byte[] of the timestamp token
* @throws IOException
*/
public byte[] getTimeStampToken(InputStream content) throws IOException {
TimeStampToken timeStampToken = tsaClient.getTimeStampToken(content);
return timeStampToken.getEncoded();
}
/**
* Extend cms signed data with TimeStamp first or to all signers
*
* @param signedData Generated CMS signed data
* @return CMSSignedData Extended CMS signed data
* @throws IOException
*/
public CMSSignedData addSignedTimeStamp(CMSSignedData signedData) throws IOException {
SignerInformationStore signerStore = signedData.getSignerInfos();
List<SignerInformation> newSigners = new ArrayList<>();
for (SignerInformation signer : signerStore.getSigners()) {
// This adds a timestamp to every signer (into his unsigned attributes) in the
// signature.
newSigners.add(signTimeStamp(signer));
}
// Because new SignerInformation is created, new SignerInfoStore has to be created
// and also be replaced in signedData. Which creates a new signedData object.
return CMSSignedData.replaceSigners(signedData, new SignerInformationStore(newSigners));
}
/**
* Extend CMS Signer Information with the TimeStampToken into the unsigned Attributes.
*
* @param signer information about signer
* @return information about SignerInformation
* @throws IOException
*/
private SignerInformation signTimeStamp(SignerInformation signer) throws IOException {
AttributeTable unsignedAttributes = signer.getUnsignedAttributes();
ASN1EncodableVector vector = new ASN1EncodableVector();
if (unsignedAttributes != null) {
vector = unsignedAttributes.toASN1EncodableVector();
}
TimeStampToken timeStampToken =
tsaClient.getTimeStampToken(new ByteArrayInputStream(signer.getSignature()));
byte[] token = timeStampToken.getEncoded();
ASN1ObjectIdentifier oid = PKCSObjectIdentifiers.id_aa_signatureTimeStampToken;
ASN1Encodable signatureTimeStamp =
new Attribute(oid, new DERSet(ASN1Primitive.fromByteArray(token)));
vector.add(signatureTimeStamp);
Attributes signedAttributes = new Attributes(vector);
// There is no other way changing the unsigned attributes of the signer information.
// result is never null, new SignerInformation always returned,
// see source code of replaceUnsignedAttributes
return SignerInformation.replaceUnsignedAttributes(
signer, new AttributeTable(signedAttributes));
}
}

View File

@@ -0,0 +1,82 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pdfbox.examples.util;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
/**
* Delegate class to close the connection when the class gets closed.
*
* @author Tilman Hausherr
*/
public class ConnectedInputStream extends InputStream {
HttpURLConnection con;
InputStream is;
public ConnectedInputStream(HttpURLConnection con, InputStream is) {
this.con = con;
this.is = is;
}
@Override
public int read() throws IOException {
return is.read();
}
@Override
public int read(byte[] b) throws IOException {
return is.read(b);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return is.read(b, off, len);
}
@Override
public long skip(long n) throws IOException {
return is.skip(n);
}
@Override
public int available() throws IOException {
return is.available();
}
@Override
public synchronized void mark(int readlimit) {
is.mark(readlimit);
}
@Override
public synchronized void reset() throws IOException {
is.reset();
}
@Override
public boolean markSupported() {
return is.markSupported();
}
@Override
public void close() throws IOException {
is.close();
con.disconnect();
}
}

View File

@@ -22,14 +22,14 @@ public class LibreOfficeListener {
private Process process; private Process process;
private LibreOfficeListener() { private LibreOfficeListener() {}
}
private boolean isListenerRunning() { private boolean isListenerRunning() {
try { try {
System.out.println("waiting for listener to start"); System.out.println("waiting for listener to start");
Socket socket = new Socket(); Socket socket = new Socket();
socket.connect(new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second socket.connect(
new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second
socket.close(); socket.close();
return true; return true;
} catch (IOException e) { } catch (IOException e) {
@@ -49,21 +49,22 @@ public class LibreOfficeListener {
// Start a background thread to monitor the activity timeout // Start a background thread to monitor the activity timeout
executorService = Executors.newSingleThreadExecutor(); executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> { executorService.submit(
while (true) { () -> {
long idleTime = System.currentTimeMillis() - lastActivityTime; while (true) {
if (idleTime >= ACTIVITY_TIMEOUT) { long idleTime = System.currentTimeMillis() - lastActivityTime;
// If there has been no activity for too long, tear down the listener if (idleTime >= ACTIVITY_TIMEOUT) {
process.destroy(); // If there has been no activity for too long, tear down the listener
break; process.destroy();
} break;
try { }
Thread.sleep(5000); // Check for inactivity every 5 seconds try {
} catch (InterruptedException e) { Thread.sleep(5000); // Check for inactivity every 5 seconds
break; } catch (InterruptedException e) {
} break;
} }
}); }
});
// Wait for the listener to start up // Wait for the listener to start up
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
@@ -92,5 +93,4 @@ public class LibreOfficeListener {
process.destroy(); process.destroy();
} }
} }
} }

View File

@@ -1,79 +1,81 @@
package stirling.software.SPDF; package stirling.software.SPDF;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Collections; import java.util.Collections;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.config.ConfigInitializer; import stirling.software.SPDF.config.ConfigInitializer;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
@SpringBootApplication
@SpringBootApplication
@EnableScheduling @EnableScheduling
public class SPdfApplication { public class SPdfApplication {
@Autowired @Autowired private Environment env;
private Environment env;
@PostConstruct
@PostConstruct public void init() {
public void init() { // Check if the BROWSER_OPEN environment variable is set to true
// Check if the BROWSER_OPEN environment variable is set to true String browserOpenEnv = env.getProperty("BROWSER_OPEN");
String browserOpenEnv = env.getProperty("BROWSER_OPEN"); boolean browserOpen = browserOpenEnv != null && browserOpenEnv.equalsIgnoreCase("true");
boolean browserOpen = browserOpenEnv != null && browserOpenEnv.equalsIgnoreCase("true");
if (browserOpen) {
if (browserOpen) { try {
try { String url = "http://localhost:" + getPort();
String url = "http://localhost:" + getPort();
String os = System.getProperty("os.name").toLowerCase();
String os = System.getProperty("os.name").toLowerCase(); Runtime rt = Runtime.getRuntime();
Runtime rt = Runtime.getRuntime(); if (os.contains("win")) {
if (os.contains("win")) { // For Windows
// For Windows rt.exec("rundll32 url.dll,FileProtocolHandler " + url);
rt.exec("rundll32 url.dll,FileProtocolHandler " + url); }
} } catch (Exception e) {
} catch (Exception e) { e.printStackTrace();
e.printStackTrace(); }
} }
} }
}
public static void main(String[] args) {
public static void main(String[] args) { SpringApplication app = new SpringApplication(SPdfApplication.class);
SpringApplication app = new SpringApplication(SPdfApplication.class); app.addInitializers(new ConfigInitializer());
app.addInitializers(new ConfigInitializer()); if (Files.exists(Paths.get("configs/settings.yml"))) {
if (Files.exists(Paths.get("configs/settings.yml"))) { app.setDefaultProperties(
app.setDefaultProperties(Collections.singletonMap("spring.config.additional-location", "file:configs/settings.yml")); Collections.singletonMap(
} else { "spring.config.additional-location", "file:configs/settings.yml"));
System.out.println("External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead."); } else {
} System.out.println(
app.run(args); "External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
}
try { app.run(args);
Thread.sleep(1000);
} catch (InterruptedException e) { try {
// TODO Auto-generated catch block Thread.sleep(1000);
e.printStackTrace(); } catch (InterruptedException e) {
} // TODO Auto-generated catch block
e.printStackTrace();
GeneralUtils.createDir("customFiles/static/"); }
GeneralUtils.createDir("customFiles/templates/");
GeneralUtils.createDir("customFiles/static/");
System.out.println("Stirling-PDF Started."); GeneralUtils.createDir("customFiles/templates/");
String url = "http://localhost:" + getPort(); System.out.println("Stirling-PDF Started.");
System.out.println("Navigate to " + url);
} String url = "http://localhost:" + getPort();
System.out.println("Navigate to " + url);
public static String getPort() { }
String port = System.getProperty("local.server.port");
if (port == null || port.isEmpty()) { public static String getPort() {
port = "8080"; String port = System.getProperty("local.server.port");
} if (port == null || port.isEmpty()) {
return port; port = "8080";
} }
} return port;
}
}

View File

@@ -1,58 +1,81 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired; import java.nio.file.Files;
import org.springframework.context.annotation.Bean; import java.nio.file.Paths;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Autowired;
import stirling.software.SPDF.model.ApplicationProperties; import org.springframework.context.annotation.Bean;
@Configuration import org.springframework.context.annotation.Configuration;
public class AppConfig {
import stirling.software.SPDF.model.ApplicationProperties;
@Autowired @Configuration
ApplicationProperties applicationProperties; public class AppConfig {
@Bean(name = "loginEnabled") @Autowired ApplicationProperties applicationProperties;
public boolean loginEnabled() {
return applicationProperties.getSecurity().getEnableLogin(); @Bean(name = "loginEnabled")
} public boolean loginEnabled() {
return applicationProperties.getSecurity().getEnableLogin();
@Bean(name = "appName") }
public String appName() {
String homeTitle = applicationProperties.getUi().getAppName(); @Bean(name = "appName")
return (homeTitle != null) ? homeTitle : "Stirling PDF"; public String appName() {
} String homeTitle = applicationProperties.getUi().getAppName();
return (homeTitle != null) ? homeTitle : "Stirling PDF";
@Bean(name = "appVersion") }
public String appVersion() {
String version = getClass().getPackage().getImplementationVersion(); @Bean(name = "appVersion")
return (version != null) ? version : "0.0.0"; public String appVersion() {
} String version = getClass().getPackage().getImplementationVersion();
return (version != null) ? version : "0.0.0";
@Bean(name = "homeText") }
public String homeText() {
return (applicationProperties.getUi().getHomeDescription() != null) ? applicationProperties.getUi().getHomeDescription() : "null"; @Bean(name = "homeText")
} public String homeText() {
return (applicationProperties.getUi().getHomeDescription() != null)
? applicationProperties.getUi().getHomeDescription()
@Bean(name = "navBarText") : "null";
public String navBarText() { }
String defaultNavBar = applicationProperties.getUi().getAppNameNavbar() != null ? applicationProperties.getUi().getAppNameNavbar() : applicationProperties.getUi().getAppName();
return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF"; @Bean(name = "navBarText")
} public String navBarText() {
String defaultNavBar =
@Bean(name = "enableAlphaFunctionality") applicationProperties.getUi().getAppNameNavbar() != null
public boolean enableAlphaFunctionality() { ? applicationProperties.getUi().getAppNameNavbar()
return applicationProperties.getSystem().getEnableAlphaFunctionality() != null ? applicationProperties.getSystem().getEnableAlphaFunctionality() : false; : applicationProperties.getUi().getAppName();
} return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF";
}
@Bean(name = "rateLimit")
public boolean rateLimit() { @Bean(name = "enableAlphaFunctionality")
String appName = System.getProperty("rateLimit"); public boolean enableAlphaFunctionality() {
if (appName == null) return applicationProperties.getSystem().getEnableAlphaFunctionality() != null
appName = System.getenv("rateLimit"); ? applicationProperties.getSystem().getEnableAlphaFunctionality()
return (appName != null) ? Boolean.valueOf(appName) : false; : false;
} }
@Bean(name = "rateLimit")
} public boolean rateLimit() {
String appName = System.getProperty("rateLimit");
if (appName == null) appName = System.getenv("rateLimit");
return (appName != null) ? Boolean.valueOf(appName) : false;
}
@Bean(name = "RunningInDocker")
public boolean runningInDocker() {
return Files.exists(Paths.get("/.dockerenv"));
}
@Bean(name = "bookFormatsInstalled")
public boolean bookFormatsInstalled() {
return applicationProperties.getSystem().getCustomApplications().isInstallBookFormats();
}
@Bean(name = "htmlFormatsInstalled")
public boolean htmlFormatsInstalled() {
return applicationProperties
.getSystem()
.getCustomApplications()
.isInstallAdvancedHtmlToPDF();
}
}

View File

@@ -1,65 +1,64 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.util.Locale; import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver; import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
public class Beans implements WebMvcConfigurer { public class Beans implements WebMvcConfigurer {
@Autowired @Autowired ApplicationProperties applicationProperties;
ApplicationProperties applicationProperties;
@Override
@Override public void addInterceptors(InterceptorRegistry registry) {
public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(localeChangeInterceptor());
registry.addInterceptor(localeChangeInterceptor()); registry.addInterceptor(new CleanUrlInterceptor());
registry.addInterceptor(new CleanUrlInterceptor()); }
}
@Bean
@Bean public LocaleChangeInterceptor localeChangeInterceptor() {
public LocaleChangeInterceptor localeChangeInterceptor() { LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
LocaleChangeInterceptor lci = new LocaleChangeInterceptor(); lci.setParamName("lang");
lci.setParamName("lang"); return lci;
return lci; }
}
@Bean
@Bean public LocaleResolver localeResolver() {
public LocaleResolver localeResolver() { SessionLocaleResolver slr = new SessionLocaleResolver();
SessionLocaleResolver slr = new SessionLocaleResolver();
String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale();
Locale defaultLocale =
String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale(); Locale.UK; // Fallback to UK locale if environment variable is not set
Locale defaultLocale = Locale.UK; // Fallback to UK locale if environment variable is not set
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) { Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
Locale tempLocale = Locale.forLanguageTag(appLocaleEnv); String tempLanguageTag = tempLocale.toLanguageTag();
String tempLanguageTag = tempLocale.toLanguageTag();
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { defaultLocale = tempLocale;
defaultLocale = tempLocale; } else {
} else { tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_", "-"));
tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_","-")); tempLanguageTag = tempLocale.toLanguageTag();
tempLanguageTag = tempLocale.toLanguageTag();
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { defaultLocale = tempLocale;
defaultLocale = tempLocale; } else {
} else { System.err.println(
System.err.println("Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK."); "Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK.");
} }
} }
} }
slr.setDefaultLocale(defaultLocale); slr.setDefaultLocale(defaultLocale);
return slr; return slr;
} }
}
}

View File

@@ -1,68 +1,74 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
public class CleanUrlInterceptor implements HandlerInterceptor { public class CleanUrlInterceptor implements HandlerInterceptor {
private static final List<String> ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error", "file", "messageType"); private static final List<String> ALLOWED_PARAMS =
Arrays.asList(
"lang", "endpoint", "endpoints", "logout", "error", "file", "messageType");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) @Override
throws Exception { public boolean preHandle(
String queryString = request.getQueryString(); HttpServletRequest request, HttpServletResponse response, Object handler)
if (queryString != null && !queryString.isEmpty()) { throws Exception {
String requestURI = request.getRequestURI(); String queryString = request.getQueryString();
Map<String, String> parameters = new HashMap<>(); if (queryString != null && !queryString.isEmpty()) {
String requestURI = request.getRequestURI();
// Keep only the allowed parameters Map<String, String> parameters = new HashMap<>();
String[] queryParameters = queryString.split("&");
for (String param : queryParameters) { // Keep only the allowed parameters
String[] keyValue = param.split("="); String[] queryParameters = queryString.split("&");
if (keyValue.length != 2) { for (String param : queryParameters) {
continue; String[] keyValue = param.split("=");
} if (keyValue.length != 2) {
if (ALLOWED_PARAMS.contains(keyValue[0])) { continue;
parameters.put(keyValue[0], keyValue[1]); }
} if (ALLOWED_PARAMS.contains(keyValue[0])) {
} parameters.put(keyValue[0], keyValue[1]);
}
// If there are any parameters that are not allowed }
if (parameters.size() != queryParameters.length) {
// Construct new query string // If there are any parameters that are not allowed
StringBuilder newQueryString = new StringBuilder(); if (parameters.size() != queryParameters.length) {
for (Map.Entry<String, String> entry : parameters.entrySet()) { // Construct new query string
if (newQueryString.length() > 0) { StringBuilder newQueryString = new StringBuilder();
newQueryString.append("&"); for (Map.Entry<String, String> entry : parameters.entrySet()) {
} if (newQueryString.length() > 0) {
newQueryString.append(entry.getKey()).append("=").append(entry.getValue()); newQueryString.append("&");
} }
newQueryString.append(entry.getKey()).append("=").append(entry.getValue());
// Redirect to the URL with only allowed query parameters }
String redirectUrl = requestURI + "?" + newQueryString;
response.sendRedirect(redirectUrl); // Redirect to the URL with only allowed query parameters
return false; String redirectUrl = requestURI + "?" + newQueryString;
} response.sendRedirect(redirectUrl);
} return false;
return true; }
} }
return true;
@Override }
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) { @Override
} public void postHandle(
HttpServletRequest request,
@Override HttpServletResponse response,
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Object handler,
Exception ex) { ModelAndView modelAndView) {}
}
} @Override
public void afterCompletion(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {}
}

View File

@@ -19,111 +19,125 @@ import java.util.stream.Collectors;
import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
public class ConfigInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { public class ConfigInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override @Override
public void initialize(ConfigurableApplicationContext applicationContext) { public void initialize(ConfigurableApplicationContext applicationContext) {
try { try {
ensureConfigExists(); ensureConfigExists();
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("Failed to initialize application configuration", e); throw new RuntimeException("Failed to initialize application configuration", e);
} }
} }
public void ensureConfigExists() throws IOException { public void ensureConfigExists() throws IOException {
// Define the path to the external config directory // Define the path to the external config directory
Path destPath = Paths.get("configs", "settings.yml"); Path destPath = Paths.get("configs", "settings.yml");
// Check if the file already exists // Check if the file already exists
if (Files.notExists(destPath)) { if (Files.notExists(destPath)) {
// Ensure the destination directory exists // Ensure the destination directory exists
Files.createDirectories(destPath.getParent()); Files.createDirectories(destPath.getParent());
// Copy the resource from classpath to the external directory // Copy the resource from classpath to the external directory
try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) { try (InputStream in =
if (in != null) { getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
Files.copy(in, destPath); if (in != null) {
} else { Files.copy(in, destPath);
throw new FileNotFoundException("Resource file not found: settings.yml.template"); } else {
} throw new FileNotFoundException(
} "Resource file not found: settings.yml.template");
} else { }
// If user file exists, we need to merge it with the template from the classpath }
List<String> templateLines; } else {
try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) { // If user file exists, we need to merge it with the template from the classpath
templateLines = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)).lines() List<String> templateLines;
.collect(Collectors.toList()); try (InputStream in =
} getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
templateLines =
new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.toList());
}
mergeYamlFiles(templateLines, destPath, destPath); mergeYamlFiles(templateLines, destPath, destPath);
} }
} }
public void mergeYamlFiles(List<String> templateLines, Path userFilePath, Path outputPath) throws IOException { public void mergeYamlFiles(List<String> templateLines, Path userFilePath, Path outputPath)
List<String> userLines = Files.readAllLines(userFilePath); throws IOException {
List<String> mergedLines = new ArrayList<>(); List<String> userLines = Files.readAllLines(userFilePath);
boolean insideAutoGenerated = false; List<String> mergedLines = new ArrayList<>();
boolean beforeFirstKey = true; boolean insideAutoGenerated = false;
boolean beforeFirstKey = true;
Function<String, Boolean> isCommented = line -> line.trim().startsWith("#"); Function<String, Boolean> isCommented = line -> line.trim().startsWith("#");
Function<String, String> extractKey = line -> { Function<String, String> extractKey =
String[] parts = line.split(":"); line -> {
return parts.length > 0 ? parts[0].trim().replace("#", "").trim() : ""; String[] parts = line.split(":");
}; return parts.length > 0 ? parts[0].trim().replace("#", "").trim() : "";
};
Set<String> userKeys = userLines.stream().map(extractKey).collect(Collectors.toSet()); Set<String> userKeys = userLines.stream().map(extractKey).collect(Collectors.toSet());
for (String line : templateLines) { for (String line : templateLines) {
String key = extractKey.apply(line); String key = extractKey.apply(line);
if (line.trim().equalsIgnoreCase("AutomaticallyGenerated:")) { if (line.trim().equalsIgnoreCase("AutomaticallyGenerated:")) {
insideAutoGenerated = true; insideAutoGenerated = true;
mergedLines.add(line); mergedLines.add(line);
continue; continue;
} else if (insideAutoGenerated && line.trim().isEmpty()) { } else if (insideAutoGenerated && line.trim().isEmpty()) {
insideAutoGenerated = false; insideAutoGenerated = false;
mergedLines.add(line); mergedLines.add(line);
continue; continue;
} }
if (beforeFirstKey && (isCommented.apply(line) || line.trim().isEmpty())) { if (beforeFirstKey && (isCommented.apply(line) || line.trim().isEmpty())) {
// Handle top comments and empty lines before the first key. // Handle top comments and empty lines before the first key.
mergedLines.add(line); mergedLines.add(line);
continue; continue;
} }
if (!key.isEmpty()) if (!key.isEmpty()) beforeFirstKey = false;
beforeFirstKey = false;
if (userKeys.contains(key)) { if (userKeys.contains(key)) {
// If user has any version (commented or uncommented) of this key, skip the // If user has any version (commented or uncommented) of this key, skip the
// template line // template line
Optional<String> userValue = userLines.stream() Optional<String> userValue =
.filter(l -> extractKey.apply(l).equalsIgnoreCase(key) && !isCommented.apply(l)).findFirst(); userLines.stream()
if (userValue.isPresent()) .filter(
mergedLines.add(userValue.get()); l ->
continue; extractKey.apply(l).equalsIgnoreCase(key)
} && !isCommented.apply(l))
.findFirst();
if (userValue.isPresent()) mergedLines.add(userValue.get());
continue;
}
if (isCommented.apply(line) || line.trim().isEmpty() || !userKeys.contains(key)) { if (isCommented.apply(line) || line.trim().isEmpty() || !userKeys.contains(key)) {
mergedLines.add(line); // If line is commented, empty or key not present in user's file, retain the mergedLines.add(
// template line line); // If line is commented, empty or key not present in user's file,
continue; // retain the
} // template line
} continue;
}
}
// Add any additional uncommented user lines that are not present in the // Add any additional uncommented user lines that are not present in the
// template // template
for (String userLine : userLines) { for (String userLine : userLines) {
String userKey = extractKey.apply(userLine); String userKey = extractKey.apply(userLine);
boolean isPresentInTemplate = templateLines.stream().map(extractKey) boolean isPresentInTemplate =
.anyMatch(templateKey -> templateKey.equalsIgnoreCase(userKey)); templateLines.stream()
if (!isPresentInTemplate && !isCommented.apply(userLine)) { .map(extractKey)
mergedLines.add(userLine); .anyMatch(templateKey -> templateKey.equalsIgnoreCase(userKey));
} if (!isPresentInTemplate && !isCommented.apply(userLine)) {
} mergedLines.add(userLine);
}
}
Files.write(outputPath, mergedLines, StandardCharsets.UTF_8); Files.write(outputPath, mergedLines, StandardCharsets.UTF_8);
} }
}
}

View File

@@ -1,241 +1,247 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.DependsOn;
import stirling.software.SPDF.model.ApplicationProperties; import org.springframework.stereotype.Service;
@Service
public class EndpointConfiguration { import stirling.software.SPDF.model.ApplicationProperties;
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>(); @Service
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>(); @DependsOn({"bookFormatsInstalled"})
public class EndpointConfiguration {
private final ApplicationProperties applicationProperties; private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
@Autowired private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
public EndpointConfiguration(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties; private final ApplicationProperties applicationProperties;
init();
processEnvironmentConfigs(); private boolean bookFormatsInstalled;
}
@Autowired
public void enableEndpoint(String endpoint) { public EndpointConfiguration(
endpointStatuses.put(endpoint, true); ApplicationProperties applicationProperties,
} @Qualifier("bookFormatsInstalled") boolean bookFormatsInstalled) {
this.applicationProperties = applicationProperties;
public void disableEndpoint(String endpoint) { this.bookFormatsInstalled = bookFormatsInstalled;
if(!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) { init();
logger.info("Disabling {}", endpoint); processEnvironmentConfigs();
endpointStatuses.put(endpoint, false); }
}
} public void enableEndpoint(String endpoint) {
endpointStatuses.put(endpoint, true);
public boolean isEndpointEnabled(String endpoint) { }
if (endpoint.startsWith("/")) {
endpoint = endpoint.substring(1); public void disableEndpoint(String endpoint) {
} if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
return endpointStatuses.getOrDefault(endpoint, true); logger.info("Disabling {}", endpoint);
} endpointStatuses.put(endpoint, false);
}
public void addEndpointToGroup(String group, String endpoint) { }
endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint);
} public boolean isEndpointEnabled(String endpoint) {
if (endpoint.startsWith("/")) {
public void enableGroup(String group) { endpoint = endpoint.substring(1);
Set<String> endpoints = endpointGroups.get(group); }
if (endpoints != null) { return endpointStatuses.getOrDefault(endpoint, true);
for (String endpoint : endpoints) { }
enableEndpoint(endpoint);
} public void addEndpointToGroup(String group, String endpoint) {
} endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint);
} }
public void disableGroup(String group) { public void enableGroup(String group) {
Set<String> endpoints = endpointGroups.get(group); Set<String> endpoints = endpointGroups.get(group);
if (endpoints != null) { if (endpoints != null) {
for (String endpoint : endpoints) { for (String endpoint : endpoints) {
disableEndpoint(endpoint); enableEndpoint(endpoint);
} }
} }
} }
public void init() { public void disableGroup(String group) {
// Adding endpoints to "PageOps" group Set<String> endpoints = endpointGroups.get(group);
addEndpointToGroup("PageOps", "remove-pages"); if (endpoints != null) {
addEndpointToGroup("PageOps", "merge-pdfs"); for (String endpoint : endpoints) {
addEndpointToGroup("PageOps", "split-pdfs"); disableEndpoint(endpoint);
addEndpointToGroup("PageOps", "pdf-organizer"); }
addEndpointToGroup("PageOps", "rotate-pdf"); }
addEndpointToGroup("PageOps", "multi-page-layout"); }
addEndpointToGroup("PageOps", "scale-pages");
addEndpointToGroup("PageOps", "adjust-contrast"); public void init() {
addEndpointToGroup("PageOps", "crop"); // Adding endpoints to "PageOps" group
addEndpointToGroup("PageOps", "auto-split-pdf"); addEndpointToGroup("PageOps", "remove-pages");
addEndpointToGroup("PageOps", "extract-page"); addEndpointToGroup("PageOps", "merge-pdfs");
addEndpointToGroup("PageOps", "pdf-to-single-page"); addEndpointToGroup("PageOps", "split-pdfs");
addEndpointToGroup("PageOps", "split-by-size-or-count"); addEndpointToGroup("PageOps", "pdf-organizer");
addEndpointToGroup("PageOps", "overlay-pdf"); addEndpointToGroup("PageOps", "rotate-pdf");
addEndpointToGroup("PageOps", "split-pdf-by-sections"); addEndpointToGroup("PageOps", "multi-page-layout");
addEndpointToGroup("PageOps", "scale-pages");
addEndpointToGroup("PageOps", "adjust-contrast");
// Adding endpoints to "Convert" group addEndpointToGroup("PageOps", "crop");
addEndpointToGroup("Convert", "pdf-to-img"); addEndpointToGroup("PageOps", "auto-split-pdf");
addEndpointToGroup("Convert", "img-to-pdf"); addEndpointToGroup("PageOps", "extract-page");
addEndpointToGroup("Convert", "pdf-to-pdfa"); addEndpointToGroup("PageOps", "pdf-to-single-page");
addEndpointToGroup("Convert", "file-to-pdf"); addEndpointToGroup("PageOps", "split-by-size-or-count");
addEndpointToGroup("Convert", "xlsx-to-pdf"); addEndpointToGroup("PageOps", "overlay-pdf");
addEndpointToGroup("Convert", "pdf-to-word"); addEndpointToGroup("PageOps", "split-pdf-by-sections");
addEndpointToGroup("Convert", "pdf-to-presentation");
addEndpointToGroup("Convert", "pdf-to-text"); // Adding endpoints to "Convert" group
addEndpointToGroup("Convert", "pdf-to-html"); addEndpointToGroup("Convert", "pdf-to-img");
addEndpointToGroup("Convert", "pdf-to-xml"); addEndpointToGroup("Convert", "img-to-pdf");
addEndpointToGroup("Convert", "html-to-pdf"); addEndpointToGroup("Convert", "pdf-to-pdfa");
addEndpointToGroup("Convert", "url-to-pdf"); addEndpointToGroup("Convert", "file-to-pdf");
addEndpointToGroup("Convert", "markdown-to-pdf"); addEndpointToGroup("Convert", "xlsx-to-pdf");
addEndpointToGroup("Convert", "pdf-to-csv"); addEndpointToGroup("Convert", "pdf-to-word");
addEndpointToGroup("Convert", "pdf-to-presentation");
addEndpointToGroup("Convert", "pdf-to-text");
// Adding endpoints to "Security" group addEndpointToGroup("Convert", "pdf-to-html");
addEndpointToGroup("Security", "add-password"); addEndpointToGroup("Convert", "pdf-to-xml");
addEndpointToGroup("Security", "remove-password"); addEndpointToGroup("Convert", "html-to-pdf");
addEndpointToGroup("Security", "change-permissions"); addEndpointToGroup("Convert", "url-to-pdf");
addEndpointToGroup("Security", "add-watermark"); addEndpointToGroup("Convert", "markdown-to-pdf");
addEndpointToGroup("Security", "cert-sign"); addEndpointToGroup("Convert", "pdf-to-csv");
addEndpointToGroup("Security", "sanitize-pdf");
addEndpointToGroup("Security", "auto-redact"); // Adding endpoints to "Security" group
addEndpointToGroup("Security", "add-password");
addEndpointToGroup("Security", "remove-password");
// Adding endpoints to "Other" group addEndpointToGroup("Security", "change-permissions");
addEndpointToGroup("Other", "ocr-pdf"); addEndpointToGroup("Security", "add-watermark");
addEndpointToGroup("Other", "add-image"); addEndpointToGroup("Security", "cert-sign");
addEndpointToGroup("Other", "compress-pdf"); addEndpointToGroup("Security", "sanitize-pdf");
addEndpointToGroup("Other", "extract-images"); addEndpointToGroup("Security", "auto-redact");
addEndpointToGroup("Other", "change-metadata");
addEndpointToGroup("Other", "extract-image-scans"); // Adding endpoints to "Other" group
addEndpointToGroup("Other", "sign"); addEndpointToGroup("Other", "ocr-pdf");
addEndpointToGroup("Other", "flatten"); addEndpointToGroup("Other", "add-image");
addEndpointToGroup("Other", "repair"); addEndpointToGroup("Other", "compress-pdf");
addEndpointToGroup("Other", "remove-blanks"); addEndpointToGroup("Other", "extract-images");
addEndpointToGroup("Other", "remove-annotations"); addEndpointToGroup("Other", "change-metadata");
addEndpointToGroup("Other", "compare"); addEndpointToGroup("Other", "extract-image-scans");
addEndpointToGroup("Other", "add-page-numbers"); addEndpointToGroup("Other", "sign");
addEndpointToGroup("Other", "auto-rename"); addEndpointToGroup("Other", "flatten");
addEndpointToGroup("Other", "get-info-on-pdf"); addEndpointToGroup("Other", "repair");
addEndpointToGroup("Other", "show-javascript"); addEndpointToGroup("Other", "remove-blanks");
addEndpointToGroup("Other", "remove-annotations");
addEndpointToGroup("Other", "compare");
addEndpointToGroup("Other", "add-page-numbers");
//CLI addEndpointToGroup("Other", "auto-rename");
addEndpointToGroup("CLI", "compress-pdf"); addEndpointToGroup("Other", "get-info-on-pdf");
addEndpointToGroup("CLI", "extract-image-scans"); addEndpointToGroup("Other", "show-javascript");
addEndpointToGroup("CLI", "remove-blanks");
addEndpointToGroup("CLI", "repair"); // CLI
addEndpointToGroup("CLI", "pdf-to-pdfa"); addEndpointToGroup("CLI", "compress-pdf");
addEndpointToGroup("CLI", "file-to-pdf"); addEndpointToGroup("CLI", "extract-image-scans");
addEndpointToGroup("CLI", "xlsx-to-pdf"); addEndpointToGroup("CLI", "remove-blanks");
addEndpointToGroup("CLI", "pdf-to-word"); addEndpointToGroup("CLI", "repair");
addEndpointToGroup("CLI", "pdf-to-presentation"); addEndpointToGroup("CLI", "pdf-to-pdfa");
addEndpointToGroup("CLI", "pdf-to-text"); addEndpointToGroup("CLI", "file-to-pdf");
addEndpointToGroup("CLI", "pdf-to-html"); addEndpointToGroup("CLI", "xlsx-to-pdf");
addEndpointToGroup("CLI", "pdf-to-xml"); addEndpointToGroup("CLI", "pdf-to-word");
addEndpointToGroup("CLI", "ocr-pdf"); addEndpointToGroup("CLI", "pdf-to-presentation");
addEndpointToGroup("CLI", "html-to-pdf"); addEndpointToGroup("CLI", "pdf-to-text");
addEndpointToGroup("CLI", "url-to-pdf"); addEndpointToGroup("CLI", "pdf-to-html");
addEndpointToGroup("CLI", "pdf-to-xml");
addEndpointToGroup("CLI", "ocr-pdf");
//python addEndpointToGroup("CLI", "html-to-pdf");
addEndpointToGroup("Python", "extract-image-scans"); addEndpointToGroup("CLI", "url-to-pdf");
addEndpointToGroup("Python", "remove-blanks"); addEndpointToGroup("CLI", "book-to-pdf");
addEndpointToGroup("Python", "html-to-pdf"); addEndpointToGroup("CLI", "pdf-to-book");
addEndpointToGroup("Python", "url-to-pdf");
// Calibre
//openCV addEndpointToGroup("Calibre", "book-to-pdf");
addEndpointToGroup("OpenCV", "extract-image-scans"); addEndpointToGroup("Calibre", "pdf-to-book");
addEndpointToGroup("OpenCV", "remove-blanks");
// python
//LibreOffice addEndpointToGroup("Python", "extract-image-scans");
addEndpointToGroup("LibreOffice", "repair"); addEndpointToGroup("Python", "remove-blanks");
addEndpointToGroup("LibreOffice", "file-to-pdf"); addEndpointToGroup("Python", "html-to-pdf");
addEndpointToGroup("LibreOffice", "xlsx-to-pdf"); addEndpointToGroup("Python", "url-to-pdf");
addEndpointToGroup("LibreOffice", "pdf-to-word");
addEndpointToGroup("LibreOffice", "pdf-to-presentation"); // openCV
addEndpointToGroup("LibreOffice", "pdf-to-text"); addEndpointToGroup("OpenCV", "extract-image-scans");
addEndpointToGroup("LibreOffice", "pdf-to-html"); addEndpointToGroup("OpenCV", "remove-blanks");
addEndpointToGroup("LibreOffice", "pdf-to-xml");
// LibreOffice
addEndpointToGroup("LibreOffice", "repair");
//OCRmyPDF addEndpointToGroup("LibreOffice", "file-to-pdf");
addEndpointToGroup("OCRmyPDF", "compress-pdf"); addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa"); addEndpointToGroup("LibreOffice", "pdf-to-word");
addEndpointToGroup("OCRmyPDF", "ocr-pdf"); addEndpointToGroup("LibreOffice", "pdf-to-presentation");
addEndpointToGroup("LibreOffice", "pdf-to-text");
//Java addEndpointToGroup("LibreOffice", "pdf-to-html");
addEndpointToGroup("Java", "merge-pdfs"); addEndpointToGroup("LibreOffice", "pdf-to-xml");
addEndpointToGroup("Java", "remove-pages");
addEndpointToGroup("Java", "split-pdfs"); // OCRmyPDF
addEndpointToGroup("Java", "pdf-organizer"); addEndpointToGroup("OCRmyPDF", "compress-pdf");
addEndpointToGroup("Java", "rotate-pdf"); addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
addEndpointToGroup("Java", "pdf-to-img"); addEndpointToGroup("OCRmyPDF", "ocr-pdf");
addEndpointToGroup("Java", "img-to-pdf");
addEndpointToGroup("Java", "add-password"); // Java
addEndpointToGroup("Java", "remove-password"); addEndpointToGroup("Java", "merge-pdfs");
addEndpointToGroup("Java", "change-permissions"); addEndpointToGroup("Java", "remove-pages");
addEndpointToGroup("Java", "add-watermark"); addEndpointToGroup("Java", "split-pdfs");
addEndpointToGroup("Java", "add-image"); addEndpointToGroup("Java", "pdf-organizer");
addEndpointToGroup("Java", "extract-images"); addEndpointToGroup("Java", "rotate-pdf");
addEndpointToGroup("Java", "change-metadata"); addEndpointToGroup("Java", "pdf-to-img");
addEndpointToGroup("Java", "cert-sign"); addEndpointToGroup("Java", "img-to-pdf");
addEndpointToGroup("Java", "multi-page-layout"); addEndpointToGroup("Java", "add-password");
addEndpointToGroup("Java", "scale-pages"); addEndpointToGroup("Java", "remove-password");
addEndpointToGroup("Java", "add-page-numbers"); addEndpointToGroup("Java", "change-permissions");
addEndpointToGroup("Java", "auto-rename"); addEndpointToGroup("Java", "add-watermark");
addEndpointToGroup("Java", "auto-split-pdf"); addEndpointToGroup("Java", "add-image");
addEndpointToGroup("Java", "sanitize-pdf"); addEndpointToGroup("Java", "extract-images");
addEndpointToGroup("Java", "crop"); addEndpointToGroup("Java", "change-metadata");
addEndpointToGroup("Java", "get-info-on-pdf"); addEndpointToGroup("Java", "cert-sign");
addEndpointToGroup("Java", "extract-page"); addEndpointToGroup("Java", "multi-page-layout");
addEndpointToGroup("Java", "pdf-to-single-page"); addEndpointToGroup("Java", "scale-pages");
addEndpointToGroup("Java", "markdown-to-pdf"); addEndpointToGroup("Java", "add-page-numbers");
addEndpointToGroup("Java", "show-javascript"); addEndpointToGroup("Java", "auto-rename");
addEndpointToGroup("Java", "auto-redact"); addEndpointToGroup("Java", "auto-split-pdf");
addEndpointToGroup("Java", "pdf-to-csv"); addEndpointToGroup("Java", "sanitize-pdf");
addEndpointToGroup("Java", "split-by-size-or-count"); addEndpointToGroup("Java", "crop");
addEndpointToGroup("Java", "overlay-pdf"); addEndpointToGroup("Java", "get-info-on-pdf");
addEndpointToGroup("Java", "split-pdf-by-sections"); addEndpointToGroup("Java", "extract-page");
addEndpointToGroup("Java", "pdf-to-single-page");
//Javascript addEndpointToGroup("Java", "markdown-to-pdf");
addEndpointToGroup("Javascript", "pdf-organizer"); addEndpointToGroup("Java", "show-javascript");
addEndpointToGroup("Javascript", "sign"); addEndpointToGroup("Java", "auto-redact");
addEndpointToGroup("Javascript", "compare"); addEndpointToGroup("Java", "pdf-to-csv");
addEndpointToGroup("Javascript", "adjust-contrast"); addEndpointToGroup("Java", "split-by-size-or-count");
addEndpointToGroup("Java", "overlay-pdf");
addEndpointToGroup("Java", "split-pdf-by-sections");
}
// Javascript
private void processEnvironmentConfigs() { addEndpointToGroup("Javascript", "pdf-organizer");
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove(); addEndpointToGroup("Javascript", "sign");
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove(); addEndpointToGroup("Javascript", "compare");
addEndpointToGroup("Javascript", "adjust-contrast");
if (endpointsToRemove != null) { }
for (String endpoint : endpointsToRemove) {
disableEndpoint(endpoint.trim()); private void processEnvironmentConfigs() {
} List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
} List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
if (!bookFormatsInstalled) {
if (groupsToRemove != null) { groupsToRemove.add("Calibre");
for (String group : groupsToRemove) { }
disableGroup(group.trim()); if (endpointsToRemove != null) {
} for (String endpoint : endpointsToRemove) {
} disableEndpoint(endpoint.trim());
} }
}
}
if (groupsToRemove != null) {
for (String group : groupsToRemove) {
disableGroup(group.trim());
}
}
}
}

View File

@@ -1,26 +1,26 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@Component @Component
public class EndpointInterceptor implements HandlerInterceptor { public class EndpointInterceptor implements HandlerInterceptor {
@Autowired @Autowired private EndpointConfiguration endpointConfiguration;
private EndpointConfiguration endpointConfiguration;
@Override
@Override public boolean preHandle(
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception { throws Exception {
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
if (!endpointConfiguration.isEndpointEnabled(requestURI)) { if (!endpointConfiguration.isEndpointEnabled(requestURI)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled"); response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled");
return false; return false;
} }
return true; return true;
} }
} }

View File

@@ -1,24 +1,25 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.config.MeterFilter; import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.config.MeterFilterReply; import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.core.instrument.config.MeterFilterReply;
@Configuration
public class MetricsConfig { @Configuration
public class MetricsConfig {
@Bean
public MeterFilter meterFilter() { @Bean
return new MeterFilter() { public MeterFilter meterFilter() {
@Override return new MeterFilter() {
public MeterFilterReply accept(Meter.Id id) { @Override
if (id.getName().equals("http.requests")) { public MeterFilterReply accept(Meter.Id id) {
return MeterFilterReply.NEUTRAL; if (id.getName().equals("http.requests")) {
} return MeterFilterReply.NEUTRAL;
return MeterFilterReply.DENY; }
} return MeterFilterReply.DENY;
}; }
} };
} }
}

View File

@@ -1,50 +1,64 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.io.IOException; import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.MeterRegistry;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class MetricsFilter extends OncePerRequestFilter { @Component
public class MetricsFilter extends OncePerRequestFilter {
private final MeterRegistry meterRegistry;
private final MeterRegistry meterRegistry;
@Autowired
public MetricsFilter(MeterRegistry meterRegistry) { @Autowired
this.meterRegistry = meterRegistry; public MetricsFilter(MeterRegistry meterRegistry) {
} this.meterRegistry = meterRegistry;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) @Override
throws ServletException, IOException { protected void doFilterInternal(
String uri = request.getRequestURI(); HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// System.out.println("uri="+uri + ", method=" + request.getMethod() ); String uri = request.getRequestURI();
// Ignore static resources
if (!(uri.startsWith("/js") || uri.startsWith("/v1/api-docs") || uri.endsWith("robots.txt") // System.out.println("uri="+uri + ", method=" + request.getMethod() );
|| uri.startsWith("/images") || uri.startsWith("/images")|| uri.endsWith(".png") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".map") // Ignore static resources
|| uri.endsWith(".svg") || uri.endsWith(".js") || uri.contains("swagger") if (!(uri.startsWith("/js")
|| uri.startsWith("/api/v1/info") || uri.startsWith("/site.webmanifest") || uri.startsWith("/fonts") || uri.startsWith("/pdfjs") )) { || uri.startsWith("/v1/api-docs")
|| uri.endsWith("robots.txt")
|| uri.startsWith("/images")
|| uri.startsWith("/images")
Counter counter = Counter.builder("http.requests").tag("uri", uri).tag("method", request.getMethod()) || uri.endsWith(".png")
.register(meterRegistry); || uri.endsWith(".ico")
|| uri.endsWith(".css")
counter.increment(); || uri.endsWith(".map")
// System.out.println("Counted"); || uri.endsWith(".svg")
} || uri.endsWith(".js")
|| uri.contains("swagger")
filterChain.doFilter(request, response); || uri.startsWith("/api/v1/info")
} || uri.startsWith("/site.webmanifest")
|| uri.startsWith("/fonts")
} || uri.startsWith("/pdfjs"))) {
Counter counter =
Counter.builder("http.requests")
.tag("uri", uri)
.tag("method", request.getMethod())
.register(meterRegistry);
counter.increment();
// System.out.println("Counted");
}
filterChain.doFilter(request, response);
}
}

View File

@@ -1,42 +1,53 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.security.SecurityScheme;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties;
@Configuration
public class OpenApiConfig { @Configuration
public class OpenApiConfig {
@Autowired
ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
@Bean @Bean
public OpenAPI customOpenAPI() { public OpenAPI customOpenAPI() {
String version = getClass().getPackage().getImplementationVersion(); String version = getClass().getPackage().getImplementationVersion();
if (version == null) { if (version == null) {
version = "1.0.0"; // default version if all else fails version = "1.0.0"; // default version if all else fails
} }
SecurityScheme apiKeyScheme = new SecurityScheme().type(SecurityScheme.Type.APIKEY).in(SecurityScheme.In.HEADER) SecurityScheme apiKeyScheme =
.name("X-API-KEY"); new SecurityScheme()
if (!applicationProperties.getSecurity().getEnableLogin()) { .type(SecurityScheme.Type.APIKEY)
return new OpenAPI().components(new Components()) .in(SecurityScheme.In.HEADER)
.info(new Info().title("Stirling PDF API").version(version).description( .name("X-API-KEY");
"API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")); if (!applicationProperties.getSecurity().getEnableLogin()) {
} else { return new OpenAPI()
return new OpenAPI().components(new Components().addSecuritySchemes("apiKey", apiKeyScheme)) .components(new Components())
.info(new Info().title("Stirling PDF API").version(version).description( .info(
"API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")) new Info()
.addSecurityItem(new SecurityRequirement().addList("apiKey")); .title("Stirling PDF API")
} .version(version)
.description(
} "API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."));
} else {
} return new OpenAPI()
.components(new Components().addSecuritySchemes("apiKey", apiKeyScheme))
.info(
new Info()
.title("Stirling PDF API")
.version(version)
.description(
"API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."))
.addSecurityItem(new SecurityRequirement().addList("apiKey"));
}
}
}

View File

@@ -0,0 +1,98 @@
package stirling.software.SPDF.config;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@Component
public class PostStartupProcesses {
@Autowired ApplicationProperties applicationProperties;
@Autowired
@Qualifier("RunningInDocker")
private boolean runningInDocker;
@Autowired
@Qualifier("bookFormatsInstalled")
private boolean bookFormatsInstalled;
@Autowired
@Qualifier("htmlFormatsInstalled")
private boolean htmlFormatsInstalled;
private static final Logger logger = LoggerFactory.getLogger(PostStartupProcesses.class);
@PostConstruct
public void runInstallCommandBasedOnEnvironment() throws IOException, InterruptedException {
List<List<String>> commands = new ArrayList<>();
// Checking for DOCKER_INSTALL_BOOK_FORMATS environment variable
if (bookFormatsInstalled) {
List<String> tmpList = new ArrayList<>();
// Set up the timezone configuration commands
tmpList.addAll(
Arrays.asList(
"sh",
"-c",
"echo 'tzdata tzdata/Areas select Europe' | debconf-set-selections; "
+ "echo 'tzdata tzdata/Zones/Europe select Berlin' | debconf-set-selections"));
commands.add(tmpList);
// Install calibre with DEBIAN_FRONTEND set to noninteractive
tmpList = new ArrayList<>();
tmpList.addAll(
Arrays.asList(
"sh",
"-c",
"DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends calibre"));
commands.add(tmpList);
}
// Checking for DOCKER_INSTALL_HTML_FORMATS environment variable
if (htmlFormatsInstalled) {
List<String> tmpList = new ArrayList<>();
// Add -y flag for automatic yes to prompts and --no-install-recommends to reduce size
tmpList.addAll(
Arrays.asList(
"apt-get", "install", "wkhtmltopdf", "-y", "--no-install-recommends"));
commands.add(tmpList);
}
if (!commands.isEmpty()) {
// Run the command
if (runningInDocker) {
List<String> tmpList = new ArrayList<>();
tmpList.addAll(Arrays.asList("apt-get", "update"));
commands.add(0, tmpList);
for (List<String> list : commands) {
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.INSTALL_APP, true)
.runCommandWithOutputHandling(list);
logger.info("RC for app installs {}", returnCode.getRc());
}
} else {
logger.info(
"Not running inside Docker so skipping automated install process with command.");
}
} else {
if (runningInDocker) {
logger.info("No custom apps to install.");
}
}
}
}

View File

@@ -1,20 +1,18 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.stereotype.Component;
import org.springframework.stereotype.Component;
@Component
@Component public class StartupApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
public class StartupApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
public static LocalDateTime startTime;
public static LocalDateTime startTime;
@Override
@Override public void onApplicationEvent(ContextRefreshedEvent event) {
public void onApplicationEvent(ContextRefreshedEvent event) { startTime = LocalDateTime.now();
startTime = LocalDateTime.now(); }
} }
}

View File

@@ -1,27 +1,26 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration @Configuration
public class WebMvcConfig implements WebMvcConfigurer { public class WebMvcConfig implements WebMvcConfigurer {
@Autowired @Autowired private EndpointInterceptor endpointInterceptor;
private EndpointInterceptor endpointInterceptor;
@Override
@Override public void addInterceptors(InterceptorRegistry registry) {
public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(endpointInterceptor);
registry.addInterceptor(endpointInterceptor); }
}
@Override
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) {
public void addResourceHandlers(ResourceHandlerRegistry registry) { // Handler for external static resources
// Handler for external static resources registry.addResourceHandler("/**")
registry.addResourceHandler("/**") .addResourceLocations("file:customFiles/static/", "classpath:/static/");
.addResourceLocations("file:customFiles/static/", "classpath:/static/"); // .setCachePeriod(0); // Optional: disable caching
//.setCachePeriod(0); // Optional: disable caching }
} }
}

View File

@@ -8,16 +8,18 @@ import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource; import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory; import org.springframework.core.io.support.PropertySourceFactory;
public class YamlPropertySourceFactory implements PropertySourceFactory { public class YamlPropertySourceFactory implements PropertySourceFactory {
@Override @Override
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource)
throws IOException { throws IOException {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(encodedResource.getResource()); factory.setResources(encodedResource.getResource());
Properties properties = factory.getObject(); Properties properties = factory.getObject();
return new PropertiesPropertySource(encodedResource.getResource().getFilename(), properties); return new PropertiesPropertySource(
encodedResource.getResource().getFilename(), properties);
} }
} }

View File

@@ -1,47 +1,49 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { @Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private final LoginAttemptService loginAttemptService; @Autowired private final LoginAttemptService loginAttemptService;
@Autowired @Autowired
public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) { public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) {
this.loginAttemptService = loginAttemptService; this.loginAttemptService = loginAttemptService;
} }
@Override @Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) public void onAuthenticationFailure(
throws IOException, ServletException { HttpServletRequest request,
String ip = request.getRemoteAddr(); HttpServletResponse response,
logger.error("Failed login attempt from IP: " + ip); AuthenticationException exception)
throws IOException, ServletException {
String username = request.getParameter("username"); String ip = request.getRemoteAddr();
if(loginAttemptService.loginAttemptCheck(username)) { logger.error("Failed login attempt from IP: " + ip);
setDefaultFailureUrl("/login?error=locked");
String username = request.getParameter("username");
} else { if (loginAttemptService.loginAttemptCheck(username)) {
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) { setDefaultFailureUrl("/login?error=locked");
setDefaultFailureUrl("/login?error=badcredentials");
} else if (exception.getClass().isAssignableFrom(LockedException.class)) { } else {
setDefaultFailureUrl("/login?error=locked"); if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) {
} setDefaultFailureUrl("/login?error=badcredentials");
} } else if (exception.getClass().isAssignableFrom(LockedException.class)) {
setDefaultFailureUrl("/login?error=locked");
}
super.onAuthenticationFailure(request, response, exception); }
}
} super.onAuthenticationFailure(request, response, exception);
}
}

View File

@@ -15,30 +15,33 @@ import jakarta.servlet.http.HttpSession;
import stirling.software.SPDF.utils.RequestUriUtils; import stirling.software.SPDF.utils.RequestUriUtils;
@Component @Component
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { public class CustomAuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired @Autowired private LoginAttemptService loginAttemptService;
private LoginAttemptService loginAttemptService;
@Override @Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { public void onAuthenticationSuccess(
String username = request.getParameter("username"); HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
String username = request.getParameter("username");
loginAttemptService.loginSucceeded(username); loginAttemptService.loginSucceeded(username);
// Get the saved request // Get the saved request
HttpSession session = request.getSession(false); HttpSession session = request.getSession(false);
SavedRequest savedRequest = session != null ? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST") : null; SavedRequest savedRequest =
if (savedRequest != null && !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) { session != null
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
: null;
if (savedRequest != null
&& !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) {
// Redirect to the original destination // Redirect to the original destination
super.onAuthenticationSuccess(request, response, authentication); super.onAuthenticationSuccess(request, response, authentication);
} else { } else {
// Redirect to the root URL (considering context path) // Redirect to the root URL (considering context path)
getRedirectStrategy().sendRedirect(request, response, "/"); getRedirectStrategy().sendRedirect(request, response, "/");
} }
//super.onAuthenticationSuccess(request, response, authentication);
}
// super.onAuthenticationSuccess(request, response, authentication);
}
} }

View File

@@ -1,52 +1,57 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.util.Collection; import java.util.Collection;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import stirling.software.SPDF.model.Authority; import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository; import stirling.software.SPDF.repository.UserRepository;
@Service @Service
public class CustomUserDetailsService implements UserDetailsService { public class CustomUserDetailsService implements UserDetailsService {
@Autowired @Autowired private UserRepository userRepository;
private UserRepository userRepository;
@Autowired private LoginAttemptService loginAttemptService;
@Autowired
private LoginAttemptService loginAttemptService; @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
@Override User user =
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { userRepository
User user = userRepository.findByUsername(username) .findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + username)); .orElseThrow(
() ->
if (loginAttemptService.isBlocked(username)) { new UsernameNotFoundException(
throw new LockedException("Your account has been locked due to too many failed login attempts."); "No user found with username: " + username));
}
if (loginAttemptService.isBlocked(username)) {
return new org.springframework.security.core.userdetails.User( throw new LockedException(
user.getUsername(), "Your account has been locked due to too many failed login attempts.");
user.getPassword(), }
user.isEnabled(),
true, true, true, return new org.springframework.security.core.userdetails.User(
getAuthorities(user.getAuthorities()) user.getUsername(),
); user.getPassword(),
} user.isEnabled(),
true,
private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) { true,
return authorities.stream() true,
.map(authority -> new SimpleGrantedAuthority(authority.getAuthority())) getAuthorities(user.getAuthorities()));
.collect(Collectors.toList()); }
}
} private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) {
return authorities.stream()
.map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
.collect(Collectors.toList());
}
}

View File

@@ -19,16 +19,16 @@ import stirling.software.SPDF.utils.RequestUriUtils;
@Component @Component
public class FirstLoginFilter extends OncePerRequestFilter { public class FirstLoginFilter extends OncePerRequestFilter {
@Autowired @Autowired @Lazy private UserService userService;
@Lazy
private UserService userService;
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { protected void doFilterInternal(
String method = request.getMethod(); HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
String requestURI = request.getRequestURI(); throws ServletException, IOException {
// Check if the request is for static resources String method = request.getMethod();
String requestURI = request.getRequestURI();
// Check if the request is for static resources
boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI); boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI);
// If it's a static resource, just continue the filter chain and skip the logic below // If it's a static resource, just continue the filter chain and skip the logic below
@@ -36,11 +36,14 @@ public class FirstLoginFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
Optional<User> user = userService.findByUsername(authentication.getName()); Optional<User> user = userService.findByUsername(authentication.getName());
if ("GET".equalsIgnoreCase(method) && user.isPresent() && user.get().isFirstLogin() && !"/change-creds".equals(requestURI)) { if ("GET".equalsIgnoreCase(method)
&& user.isPresent()
&& user.get().isFirstLogin()
&& !"/change-creds".equals(requestURI)) {
response.sendRedirect("/change-creds"); response.sendRedirect("/change-creds");
return; return;
} }

View File

@@ -1,4 +1,5 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@@ -13,51 +14,53 @@ import stirling.software.SPDF.utils.RequestUriUtils;
public class IPRateLimitingFilter implements Filter { public class IPRateLimitingFilter implements Filter {
private final ConcurrentHashMap<String, AtomicInteger> requestCounts = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, AtomicInteger> requestCounts =
new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, AtomicInteger> getCounts = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, AtomicInteger> getCounts = new ConcurrentHashMap<>();
private final int maxRequests; private final int maxRequests;
private final int maxGetRequests; private final int maxGetRequests;
public IPRateLimitingFilter(int maxRequests, int maxGetRequests) { public IPRateLimitingFilter(int maxRequests, int maxGetRequests) {
this.maxRequests = maxRequests; this.maxRequests = maxRequests;
this.maxGetRequests = maxGetRequests; this.maxGetRequests = maxGetRequests;
} }
@Override @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
if (request instanceof HttpServletRequest) { throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request; if (request instanceof HttpServletRequest) {
String method = httpRequest.getMethod(); HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI(); String method = httpRequest.getMethod();
// Check if the request is for static resources String requestURI = httpRequest.getRequestURI();
boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI); // Check if the request is for static resources
boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI);
// If it's a static resource, just continue the filter chain and skip the logic below // If it's a static resource, just continue the filter chain and skip the logic below
if (isStaticResource) { if (isStaticResource) {
chain.doFilter(request, response); chain.doFilter(request, response);
return; return;
} }
String clientIp = request.getRemoteAddr(); String clientIp = request.getRemoteAddr();
requestCounts.computeIfAbsent(clientIp, k -> new AtomicInteger(0)); requestCounts.computeIfAbsent(clientIp, k -> new AtomicInteger(0));
if (!"GET".equalsIgnoreCase(method)) { if (!"GET".equalsIgnoreCase(method)) {
if (requestCounts.get(clientIp).incrementAndGet() > maxRequests) { if (requestCounts.get(clientIp).incrementAndGet() > maxRequests) {
// Handle limit exceeded (e.g., send error response) // Handle limit exceeded (e.g., send error response)
response.getWriter().write("Rate limit exceeded"); response.getWriter().write("Rate limit exceeded");
return; return;
} }
} else { } else {
if (requestCounts.get(clientIp).incrementAndGet() > maxGetRequests) { if (requestCounts.get(clientIp).incrementAndGet() > maxGetRequests) {
// Handle limit exceeded (e.g., send error response) // Handle limit exceeded (e.g., send error response)
response.getWriter().write("GET Rate limit exceeded"); response.getWriter().write("GET Rate limit exceeded");
return; return;
} }
} }
} }
chain.doFilter(request, response); chain.doFilter(request, response);
} }
public void resetRequestCounts() { public void resetRequestCounts() {
requestCounts.clear(); requestCounts.clear();
getCounts.clear(); getCounts.clear();

View File

@@ -13,75 +13,76 @@ import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
@Component @Component
public class InitialSecuritySetup { public class InitialSecuritySetup {
@Autowired @Autowired private UserService userService;
private UserService userService;
@Autowired ApplicationProperties applicationProperties;
@Autowired @PostConstruct
ApplicationProperties applicationProperties; public void init() {
if (!userService.hasUsers()) {
@PostConstruct
public void init() {
if (!userService.hasUsers()) {
String initialUsername = applicationProperties.getSecurity().getInitialLogin().getUsername();
String initialPassword = applicationProperties.getSecurity().getInitialLogin().getPassword();
if (initialUsername != null && initialPassword != null) {
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
} else {
initialUsername = "admin";
initialPassword = "stirling";
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId(), true);
}
}
if(!userService.usernameExists(Role.INTERNAL_API_USER.getRoleId())) {
userService.saveUser(Role.INTERNAL_API_USER.getRoleId(), UUID.randomUUID().toString(), Role.INTERNAL_API_USER.getRoleId());
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
}
}
String initialUsername =
applicationProperties.getSecurity().getInitialLogin().getUsername();
String initialPassword =
applicationProperties.getSecurity().getInitialLogin().getPassword();
if (initialUsername != null && initialPassword != null) {
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
} else {
initialUsername = "admin";
initialPassword = "stirling";
userService.saveUser(
initialUsername, initialPassword, Role.ADMIN.getRoleId(), true);
}
}
if (!userService.usernameExists(Role.INTERNAL_API_USER.getRoleId())) {
userService.saveUser(
Role.INTERNAL_API_USER.getRoleId(),
UUID.randomUUID().toString(),
Role.INTERNAL_API_USER.getRoleId());
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
}
}
@PostConstruct
public void initSecretKey() throws IOException {
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
if (secretKey == null || secretKey.isEmpty()) {
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
saveKeyToConfig(secretKey);
}
}
@PostConstruct private void saveKeyToConfig(String key) throws IOException {
public void initSecretKey() throws IOException { Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey(); List<String> lines = Files.readAllLines(path);
if (secretKey == null || secretKey.isEmpty()) { boolean keyFound = false;
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
saveKeyToConfig(secretKey);
}
}
private void saveKeyToConfig(String key) throws IOException { // Search for the existing key to replace it or place to add it
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml for (int i = 0; i < lines.size(); i++) {
List<String> lines = Files.readAllLines(path); if (lines.get(i).startsWith("AutomaticallyGenerated:")) {
boolean keyFound = false; keyFound = true;
if (i + 1 < lines.size() && lines.get(i + 1).trim().startsWith("key:")) {
lines.set(i + 1, " key: " + key);
break;
} else {
lines.add(i + 1, " key: " + key);
break;
}
}
}
// Search for the existing key to replace it or place to add it // If the section doesn't exist, append it
for (int i = 0; i < lines.size(); i++) { if (!keyFound) {
if (lines.get(i).startsWith("AutomaticallyGenerated:")) { lines.add("# Automatically Generated Settings (Do Not Edit Directly)");
keyFound = true; lines.add("AutomaticallyGenerated:");
if (i + 1 < lines.size() && lines.get(i + 1).trim().startsWith("key:")) { lines.add(" key: " + key);
lines.set(i + 1, " key: " + key); }
break;
} else {
lines.add(i + 1, " key: " + key);
break;
}
}
}
// If the section doesn't exist, append it // Write back to the file
if (!keyFound) { Files.write(path, lines);
lines.add("# Automatically Generated Settings (Do Not Edit Directly)"); }
lines.add("AutomaticallyGenerated:"); }
lines.add(" key: " + key);
}
// Write back to the file
Files.write(path, lines);
}
}

View File

@@ -1,4 +1,5 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -12,39 +13,41 @@ import stirling.software.SPDF.model.AttemptCounter;
@Service @Service
public class LoginAttemptService { public class LoginAttemptService {
@Autowired ApplicationProperties applicationProperties;
@Autowired
ApplicationProperties applicationProperties;
private int MAX_ATTEMPTS; private int MAX_ATTEMPTS;
private long ATTEMPT_INCREMENT_TIME; private long ATTEMPT_INCREMENT_TIME;
@PostConstruct @PostConstruct
public void init() { public void init() {
MAX_ATTEMPTS = applicationProperties.getSecurity().getLoginAttemptCount(); MAX_ATTEMPTS = applicationProperties.getSecurity().getLoginAttemptCount();
ATTEMPT_INCREMENT_TIME = TimeUnit.MINUTES.toMillis(applicationProperties.getSecurity().getLoginResetTimeMinutes()); ATTEMPT_INCREMENT_TIME =
TimeUnit.MINUTES.toMillis(
applicationProperties.getSecurity().getLoginResetTimeMinutes());
} }
private final ConcurrentHashMap<String, AttemptCounter> attemptsCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, AttemptCounter> attemptsCache =
new ConcurrentHashMap<>();
public void loginSucceeded(String key) { public void loginSucceeded(String key) {
attemptsCache.remove(key); attemptsCache.remove(key);
} }
public boolean loginAttemptCheck(String key) { public boolean loginAttemptCheck(String key) {
attemptsCache.compute(key, (k, attemptCounter) -> { attemptsCache.compute(
if (attemptCounter == null || attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) { key,
return new AttemptCounter(); (k, attemptCounter) -> {
} else { if (attemptCounter == null
attemptCounter.increment(); || attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) {
return attemptCounter; return new AttemptCounter();
} } else {
}); attemptCounter.increment();
return attemptCounter;
}
});
return attemptsCache.get(key).getAttemptCount() >= MAX_ATTEMPTS; return attemptsCache.get(key).getAttemptCount() >= MAX_ATTEMPTS;
} }
public boolean isBlocked(String key) { public boolean isBlocked(String key) {
AttemptCounter attemptCounter = attemptsCache.get(key); AttemptCounter attemptCounter = attemptsCache.get(key);
if (attemptCounter != null) { if (attemptCounter != null) {
@@ -52,5 +55,4 @@ public class LoginAttemptService {
} }
return false; return false;
} }
} }

View File

@@ -1,4 +1,5 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -11,7 +12,7 @@ public class RateLimitResetScheduler {
this.rateLimitingFilter = rateLimitingFilter; this.rateLimitingFilter = rateLimitingFilter;
} }
@Scheduled(cron = "0 0 0 * * MON") // At 00:00 every Monday TODO: configurable @Scheduled(cron = "0 0 0 * * MON") // At 00:00 every Monday TODO: configurable
public void resetRateLimit() { public void resetRateLimit() {
rateLimitingFilter.resetRequestCounts(); rateLimitingFilter.resetRequestCounts();
} }

View File

@@ -1,136 +1,140 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.savedrequest.NullRequestCache; import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import stirling.software.SPDF.repository.JPATokenRepositoryImpl; import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
@Configuration
@EnableWebSecurity() @Configuration
@EnableMethodSecurity @EnableWebSecurity()
public class SecurityConfiguration { @EnableMethodSecurity
public class SecurityConfiguration {
@Autowired
private UserDetailsService userDetailsService; @Autowired private UserDetailsService userDetailsService;
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();
} }
@Autowired
@Lazy @Autowired @Lazy private UserService userService;
private UserService userService;
@Autowired
@Autowired @Qualifier("loginEnabled")
@Qualifier("loginEnabled") public boolean loginEnabledValue;
public boolean loginEnabledValue;
@Autowired private UserAuthenticationFilter userAuthenticationFilter;
@Autowired
private UserAuthenticationFilter userAuthenticationFilter; @Autowired private LoginAttemptService loginAttemptService;
@Autowired private FirstLoginFilter firstLoginFilter;
@Autowired @Bean
private LoginAttemptService loginAttemptService; public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
@Autowired
private FirstLoginFilter firstLoginFilter; if (loginEnabledValue) {
@Bean http.csrf(csrf -> csrf.disable());
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
http.formLogin(
if(loginEnabledValue) { formLogin ->
formLogin
http.csrf(csrf -> csrf.disable()); .loginPage("/login")
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class); .successHandler(
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class); new CustomAuthenticationSuccessHandler())
http .defaultSuccessUrl("/")
.formLogin(formLogin -> formLogin .failureHandler(
.loginPage("/login") new CustomAuthenticationFailureHandler(
.successHandler(new CustomAuthenticationSuccessHandler()) loginAttemptService))
.defaultSuccessUrl("/") .permitAll())
.failureHandler(new CustomAuthenticationFailureHandler(loginAttemptService)) .requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()))
.permitAll() .logout(
).requestCache(requestCache -> requestCache logout ->
.requestCache(new NullRequestCache()) logout.logoutRequestMatcher(
) new AntPathRequestMatcher("/logout"))
.logout(logout -> logout .logoutSuccessUrl("/login?logout=true")
.logoutRequestMatcher(new AntPathRequestMatcher("/logout")) .invalidateHttpSession(true) // Invalidate session
.logoutSuccessUrl("/login?logout=true") .deleteCookies("JSESSIONID", "remember-me"))
.invalidateHttpSession(true) // Invalidate session .rememberMe(
.deleteCookies("JSESSIONID", "remember-me") rememberMeConfigurer ->
).rememberMe(rememberMeConfigurer -> rememberMeConfigurer // Use the configurator directly rememberMeConfigurer // Use the configurator directly
.key("uniqueAndSecret") .key("uniqueAndSecret")
.tokenRepository(persistentTokenRepository()) .tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(1209600) // 2 weeks .tokenValiditySeconds(1209600) // 2 weeks
) )
.authorizeHttpRequests(authz -> authz .authorizeHttpRequests(
.requestMatchers(req -> { authz ->
String uri = req.getRequestURI(); authz.requestMatchers(
String contextPath = req.getContextPath(); req -> {
String uri = req.getRequestURI();
// Remove the context path from the URI String contextPath = req.getContextPath();
String trimmedUri = uri.startsWith(contextPath) ? uri.substring(contextPath.length()) : uri;
// Remove the context path from the URI
return trimmedUri.startsWith("/login") || trimmedUri.endsWith(".svg") || String trimmedUri =
trimmedUri.startsWith("/register") || trimmedUri.startsWith("/error") || uri.startsWith(contextPath)
trimmedUri.startsWith("/images/") || trimmedUri.startsWith("/public/") || ? uri.substring(
trimmedUri.startsWith("/css/") || trimmedUri.startsWith("/js/"); contextPath
} .length())
).permitAll() : uri;
.anyRequest().authenticated()
) return trimmedUri.startsWith("/login")
.userDetailsService(userDetailsService) || trimmedUri.endsWith(".svg")
.authenticationProvider(authenticationProvider()); || trimmedUri.startsWith(
} else { "/register")
http.csrf(csrf -> csrf.disable()) || trimmedUri.startsWith("/error")
.authorizeHttpRequests(authz -> authz || trimmedUri.startsWith("/images/")
.anyRequest().permitAll() || trimmedUri.startsWith("/public/")
); || trimmedUri.startsWith("/css/")
} || trimmedUri.startsWith("/js/")
return http.build(); || trimmedUri.startsWith(
} "/api/v1/info/status");
})
.permitAll()
.anyRequest()
.authenticated())
@Bean .userDetailsService(userDetailsService)
public IPRateLimitingFilter rateLimitingFilter() { .authenticationProvider(authenticationProvider());
int maxRequestsPerIp = 1000000; // Example limit TODO add config level } else {
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp); http.csrf(csrf -> csrf.disable())
} .authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
}
return http.build();
@Bean }
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); @Bean
authProvider.setUserDetailsService(userDetailsService); public IPRateLimitingFilter rateLimitingFilter() {
authProvider.setPasswordEncoder(passwordEncoder()); int maxRequestsPerIp = 1000000; // Example limit TODO add config level
return authProvider; return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
} }
@Bean @Bean
public PersistentTokenRepository persistentTokenRepository() { public DaoAuthenticationProvider authenticationProvider() {
return new JPATokenRepositoryImpl(); DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
} authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
return new JPATokenRepositoryImpl();
}
}

View File

@@ -1,117 +1,118 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.model.ApiKeyAuthenticationToken; import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
@Component
public class UserAuthenticationFilter extends OncePerRequestFilter { @Component
public class UserAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService; @Autowired private UserDetailsService userDetailsService;
@Autowired @Autowired @Lazy private UserService userService;
@Lazy
private UserService userService; @Autowired
@Qualifier("loginEnabled")
public boolean loginEnabledValue;
@Autowired
@Qualifier("loginEnabled") @Override
public boolean loginEnabledValue; protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
@Override throws ServletException, IOException {
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, if (!loginEnabledValue) {
FilterChain filterChain) throws ServletException, IOException { // If login is not enabled, just pass all requests without authentication
filterChain.doFilter(request, response);
if (!loginEnabledValue) { return;
// If login is not enabled, just pass all requests without authentication }
filterChain.doFilter(request, response); String requestURI = request.getRequestURI();
return; Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
}
String requestURI = request.getRequestURI(); // Check for API key in the request headers if no authentication exists
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !authentication.isAuthenticated()) {
String apiKey = request.getHeader("X-API-Key");
// Check for API key in the request headers if no authentication exists if (apiKey != null && !apiKey.trim().isEmpty()) {
if (authentication == null || !authentication.isAuthenticated()) { try {
String apiKey = request.getHeader("X-API-Key"); // Use API key to authenticate. This requires you to have an authentication
if (apiKey != null && !apiKey.trim().isEmpty()) { // provider for API keys.
try { UserDetails userDetails = userService.loadUserByApiKey(apiKey);
// Use API key to authenticate. This requires you to have an authentication provider for API keys. if (userDetails == null) {
UserDetails userDetails = userService.loadUserByApiKey(apiKey); response.setStatus(HttpStatus.UNAUTHORIZED.value());
if(userDetails == null) response.getWriter().write("Invalid API Key.");
{ return;
response.setStatus(HttpStatus.UNAUTHORIZED.value()); }
response.getWriter().write("Invalid API Key."); authentication =
return; new ApiKeyAuthenticationToken(
} userDetails, apiKey, userDetails.getAuthorities());
authentication = new ApiKeyAuthenticationToken(userDetails, apiKey, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication); } catch (AuthenticationException e) {
} catch (AuthenticationException e) { // If API key authentication fails, deny the request
// If API key authentication fails, deny the request response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write("Invalid API Key.");
response.getWriter().write("Invalid API Key."); return;
return; }
} }
} }
}
// If we still don't have any authentication, deny the request
// If we still don't have any authentication, deny the request if (authentication == null || !authentication.isAuthenticated()) {
if (authentication == null || !authentication.isAuthenticated()) { String method = request.getMethod();
String method = request.getMethod(); String contextPath = request.getContextPath();
String contextPath = request.getContextPath();
if ("GET".equalsIgnoreCase(method) && !(contextPath + "/login").equals(requestURI)) {
if ("GET".equalsIgnoreCase(method) && ! (contextPath + "/login").equals(requestURI)) { response.sendRedirect(contextPath + "/login"); // redirect to the login page
response.sendRedirect(contextPath + "/login"); // redirect to the login page return;
return; } else {
} else { response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter()
response.getWriter().write("Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected"); .write(
return; "Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected");
} return;
} }
}
filterChain.doFilter(request, response);
} filterChain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { @Override
String uri = request.getRequestURI(); protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String contextPath = request.getContextPath(); String uri = request.getRequestURI();
String[] permitAllPatterns = { String contextPath = request.getContextPath();
contextPath + "/login", String[] permitAllPatterns = {
contextPath + "/register", contextPath + "/login",
contextPath + "/error", contextPath + "/register",
contextPath + "/images/", contextPath + "/error",
contextPath + "/public/", contextPath + "/images/",
contextPath + "/css/", contextPath + "/public/",
contextPath + "/js/", contextPath + "/css/",
contextPath + "/pdfjs/", contextPath + "/js/",
contextPath + "/site.webmanifest" contextPath + "/pdfjs/",
}; contextPath + "/api/v1/info/status",
contextPath + "/site.webmanifest"
for (String pattern : permitAllPatterns) { };
if (uri.startsWith(pattern) || uri.endsWith(".svg")) {
return true; for (String pattern : permitAllPatterns) {
} if (uri.startsWith(pattern) || uri.endsWith(".svg")) {
} return true;
}
return false; }
}
return false;
} }
}

View File

@@ -1,125 +1,143 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import java.time.Duration; import java.time.Duration;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import io.github.bucket4j.Bandwidth; import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket; import io.github.bucket4j.Bucket;
import io.github.bucket4j.ConsumptionProbe; import io.github.bucket4j.ConsumptionProbe;
import io.github.bucket4j.Refill; import io.github.bucket4j.Refill;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletRequest;
import stirling.software.SPDF.model.Role; import jakarta.servlet.http.HttpServletResponse;
@Component import stirling.software.SPDF.model.Role;
public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
@Component
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>(); public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>();
@Autowired private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
private UserDetailsService userDetailsService;
@Autowired private UserDetailsService userDetailsService;
@Autowired
@Qualifier("rateLimit") @Autowired
public boolean rateLimit; @Qualifier("rateLimit")
public boolean rateLimit;
@Override
protected void doFilterInternal(HttpServletRequest request, @Override
HttpServletResponse response, protected void doFilterInternal(
FilterChain filterChain) throws ServletException, IOException { HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
if (!rateLimit) { throws ServletException, IOException {
// If rateLimit is not enabled, just pass all requests without rate limiting if (!rateLimit) {
filterChain.doFilter(request, response); // If rateLimit is not enabled, just pass all requests without rate limiting
return; filterChain.doFilter(request, response);
} return;
}
String method = request.getMethod();
if (!"POST".equalsIgnoreCase(method)) { String method = request.getMethod();
// If the request is not a POST, just pass it through without rate limiting if (!"POST".equalsIgnoreCase(method)) {
filterChain.doFilter(request, response); // If the request is not a POST, just pass it through without rate limiting
return; filterChain.doFilter(request, response);
} return;
}
String identifier = null;
String identifier = null;
// Check for API key in the request headers
String apiKey = request.getHeader("X-API-Key"); // Check for API key in the request headers
if (apiKey != null && !apiKey.trim().isEmpty()) { String apiKey = request.getHeader("X-API-Key");
identifier = "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames if (apiKey != null && !apiKey.trim().isEmpty()) {
} else { identifier =
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
if (authentication != null && authentication.isAuthenticated()) { } else {
UserDetails userDetails = (UserDetails) authentication.getPrincipal(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
identifier = userDetails.getUsername(); if (authentication != null && authentication.isAuthenticated()) {
} UserDetails userDetails = (UserDetails) authentication.getPrincipal();
} identifier = userDetails.getUsername();
}
// If neither API key nor an authenticated user is present, use IP address }
if (identifier == null) {
identifier = request.getRemoteAddr(); // If neither API key nor an authenticated user is present, use IP address
} if (identifier == null) {
identifier = request.getRemoteAddr();
Role userRole = getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication()); }
if (request.getHeader("X-API-Key") != null) { Role userRole =
// It's an API call getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
processRequest(userRole.getApiCallsPerDay(), identifier, apiBuckets, request, response, filterChain);
} else { if (request.getHeader("X-API-Key") != null) {
// It's a Web UI call // It's an API call
processRequest(userRole.getWebCallsPerDay(), identifier, webBuckets, request, response, filterChain); processRequest(
} userRole.getApiCallsPerDay(),
} identifier,
apiBuckets,
private Role getRoleFromAuthentication(Authentication authentication) { request,
if (authentication != null && authentication.isAuthenticated()) { response,
for (GrantedAuthority authority : authentication.getAuthorities()) { filterChain);
try { } else {
return Role.fromString(authority.getAuthority()); // It's a Web UI call
} catch (IllegalArgumentException ex) { processRequest(
// Ignore and continue to next authority. userRole.getWebCallsPerDay(),
} identifier,
} webBuckets,
} request,
throw new IllegalStateException("User does not have a valid role."); response,
} filterChain);
}
private void processRequest(int limitPerDay, String identifier, Map<String, Bucket> buckets, }
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException { private Role getRoleFromAuthentication(Authentication authentication) {
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay)); if (authentication != null && authentication.isAuthenticated()) {
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1); for (GrantedAuthority authority : authentication.getAuthorities()) {
try {
if (probe.isConsumed()) { return Role.fromString(authority.getAuthority());
response.setHeader("X-Rate-Limit-Remaining", Long.toString(probe.getRemainingTokens())); } catch (IllegalArgumentException ex) {
filterChain.doFilter(request, response); // Ignore and continue to next authority.
} else { }
long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000; }
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); }
response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill)); throw new IllegalStateException("User does not have a valid role.");
response.getWriter().write("Rate limit exceeded for POST requests."); }
}
} private void processRequest(
int limitPerDay,
private Bucket createUserBucket(int limitPerDay) { String identifier,
Bandwidth limit = Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1))); Map<String, Bucket> buckets,
return Bucket.builder().addLimit(limit).build(); HttpServletRequest request,
} HttpServletResponse response,
} FilterChain filterChain)
throws IOException, ServletException {
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay));
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
if (probe.isConsumed()) {
response.setHeader("X-Rate-Limit-Remaining", Long.toString(probe.getRemainingTokens()));
filterChain.doFilter(request, response);
} else {
long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill));
response.getWriter().write("Rate limit exceeded for POST requests.");
}
}
private Bucket createUserBucket(int limitPerDay) {
Bandwidth limit =
Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1)));
return Bucket.builder().addLimit(limit).build();
}
}

View File

@@ -1,198 +1,197 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.util.Collection;
import java.util.HashMap; import java.util.Collection;
import java.util.Map; import java.util.HashMap;
import java.util.Optional; import java.util.Map;
import java.util.UUID; import java.util.Optional;
import java.util.stream.Collectors; import java.util.UUID;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.Authority; import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.repository.UserRepository; import stirling.software.SPDF.model.User;
@Service import stirling.software.SPDF.repository.UserRepository;
public class UserService implements UserServiceInterface{
@Service
@Autowired public class UserService implements UserServiceInterface {
private UserRepository userRepository;
@Autowired private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder; @Autowired private PasswordEncoder passwordEncoder;
public Authentication getAuthentication(String apiKey) { public Authentication getAuthentication(String apiKey) {
User user = getUserByApiKey(apiKey); User user = getUserByApiKey(apiKey);
if (user == null) { if (user == null) {
throw new UsernameNotFoundException("API key is not valid"); throw new UsernameNotFoundException("API key is not valid");
} }
// Convert the user into an Authentication object // Convert the user into an Authentication object
return new UsernamePasswordAuthenticationToken( return new UsernamePasswordAuthenticationToken(
user, // principal (typically the user) user, // principal (typically the user)
null, // credentials (we don't expose the password or API key here) null, // credentials (we don't expose the password or API key here)
getAuthorities(user) // user's authorities (roles/permissions) getAuthorities(user) // user's authorities (roles/permissions)
); );
} }
private Collection<? extends GrantedAuthority> getAuthorities(User user) { private Collection<? extends GrantedAuthority> getAuthorities(User user) {
// Convert each Authority object into a SimpleGrantedAuthority object. // Convert each Authority object into a SimpleGrantedAuthority object.
return user.getAuthorities().stream() return user.getAuthorities().stream()
.map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority())) .map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority()))
.collect(Collectors.toList()); .collect(Collectors.toList());
}
} private String generateApiKey() {
String apiKey;
private String generateApiKey() { do {
String apiKey; apiKey = UUID.randomUUID().toString();
do { } while (userRepository.findByApiKey(apiKey) != null); // Ensure uniqueness
apiKey = UUID.randomUUID().toString(); return apiKey;
} while (userRepository.findByApiKey(apiKey) != null); // Ensure uniqueness }
return apiKey;
} public User addApiKeyToUser(String username) {
User user =
public User addApiKeyToUser(String username) { userRepository
User user = userRepository.findByUsername(username) .findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found")); .orElseThrow(() -> new UsernameNotFoundException("User not found"));
user.setApiKey(generateApiKey()); user.setApiKey(generateApiKey());
return userRepository.save(user); return userRepository.save(user);
} }
public User refreshApiKeyForUser(String username) { public User refreshApiKeyForUser(String username) {
return addApiKeyToUser(username); // reuse the add API key method for refreshing return addApiKeyToUser(username); // reuse the add API key method for refreshing
} }
public String getApiKeyForUser(String username) { public String getApiKeyForUser(String username) {
User user = userRepository.findByUsername(username) User user =
.orElseThrow(() -> new UsernameNotFoundException("User not found")); userRepository
return user.getApiKey(); .findByUsername(username)
} .orElseThrow(() -> new UsernameNotFoundException("User not found"));
return user.getApiKey();
public boolean isValidApiKey(String apiKey) { }
return userRepository.findByApiKey(apiKey) != null;
} public boolean isValidApiKey(String apiKey) {
return userRepository.findByApiKey(apiKey) != null;
public User getUserByApiKey(String apiKey) { }
return userRepository.findByApiKey(apiKey);
} public User getUserByApiKey(String apiKey) {
return userRepository.findByApiKey(apiKey);
public UserDetails loadUserByApiKey(String apiKey) { }
User userOptional = userRepository.findByApiKey(apiKey);
if (userOptional != null) { public UserDetails loadUserByApiKey(String apiKey) {
User user = userOptional; User userOptional = userRepository.findByApiKey(apiKey);
// Convert your User entity to a UserDetails object with authorities if (userOptional != null) {
return new org.springframework.security.core.userdetails.User( User user = userOptional;
user.getUsername(), // Convert your User entity to a UserDetails object with authorities
user.getPassword(), // you might not need this for API key auth return new org.springframework.security.core.userdetails.User(
getAuthorities(user) user.getUsername(),
); user.getPassword(), // you might not need this for API key auth
} getAuthorities(user));
return null; // or throw an exception }
} return null; // or throw an exception
}
public boolean validateApiKeyForUser(String username, String apiKey) { public boolean validateApiKeyForUser(String username, String apiKey) {
Optional<User> userOpt = userRepository.findByUsername(username); Optional<User> userOpt = userRepository.findByUsername(username);
return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey); return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey);
} }
public void saveUser(String username, String password) { public void saveUser(String username, String password) {
User user = new User(); User user = new User();
user.setUsername(username); user.setUsername(username);
user.setPassword(passwordEncoder.encode(password)); user.setPassword(passwordEncoder.encode(password));
user.setEnabled(true); user.setEnabled(true);
userRepository.save(user); userRepository.save(user);
} }
public void saveUser(String username, String password, String role, boolean firstLogin) { public void saveUser(String username, String password, String role, boolean firstLogin) {
User user = new User(); User user = new User();
user.setUsername(username); user.setUsername(username);
user.setPassword(passwordEncoder.encode(password)); user.setPassword(passwordEncoder.encode(password));
user.addAuthority(new Authority(role, user)); user.addAuthority(new Authority(role, user));
user.setEnabled(true); user.setEnabled(true);
user.setFirstLogin(firstLogin); user.setFirstLogin(firstLogin);
userRepository.save(user); userRepository.save(user);
} }
public void saveUser(String username, String password, String role) { public void saveUser(String username, String password, String role) {
User user = new User(); User user = new User();
user.setUsername(username); user.setUsername(username);
user.setPassword(passwordEncoder.encode(password)); user.setPassword(passwordEncoder.encode(password));
user.addAuthority(new Authority(role, user)); user.addAuthority(new Authority(role, user));
user.setEnabled(true); user.setEnabled(true);
user.setFirstLogin(false); user.setFirstLogin(false);
userRepository.save(user); userRepository.save(user);
} }
public void deleteUser(String username) { public void deleteUser(String username) {
Optional<User> userOpt = userRepository.findByUsername(username); Optional<User> userOpt = userRepository.findByUsername(username);
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
for (Authority authority : userOpt.get().getAuthorities()) { for (Authority authority : userOpt.get().getAuthorities()) {
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) { if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
return; return;
} }
} }
userRepository.delete(userOpt.get()); userRepository.delete(userOpt.get());
} }
} }
public boolean usernameExists(String username) { public boolean usernameExists(String username) {
return userRepository.findByUsername(username).isPresent(); return userRepository.findByUsername(username).isPresent();
} }
public boolean hasUsers() { public boolean hasUsers() {
return userRepository.count() > 0; return userRepository.count() > 0;
} }
public void updateUserSettings(String username, Map<String, String> updates) { public void updateUserSettings(String username, Map<String, String> updates) {
Optional<User> userOpt = userRepository.findByUsername(username); Optional<User> userOpt = userRepository.findByUsername(username);
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
User user = userOpt.get(); User user = userOpt.get();
Map<String, String> settingsMap = user.getSettings(); Map<String, String> settingsMap = user.getSettings();
if(settingsMap == null) { if (settingsMap == null) {
settingsMap = new HashMap<String,String>(); settingsMap = new HashMap<String, String>();
} }
settingsMap.clear(); settingsMap.clear();
settingsMap.putAll(updates); settingsMap.putAll(updates);
user.setSettings(settingsMap); user.setSettings(settingsMap);
userRepository.save(user); userRepository.save(user);
} }
} }
public Optional<User> findByUsername(String username) { public Optional<User> findByUsername(String username) {
return userRepository.findByUsername(username); return userRepository.findByUsername(username);
} }
public void changeUsername(User user, String newUsername) { public void changeUsername(User user, String newUsername) {
user.setUsername(newUsername); user.setUsername(newUsername);
userRepository.save(user); userRepository.save(user);
} }
public void changePassword(User user, String newPassword) { public void changePassword(User user, String newPassword) {
user.setPassword(passwordEncoder.encode(newPassword)); user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user); userRepository.save(user);
} }
public void changeFirstUse(User user, boolean firstUse) { public void changeFirstUse(User user, boolean firstUse) {
user.setFirstLogin(firstUse); user.setFirstLogin(firstUse);
userRepository.save(user); userRepository.save(user);
} }
public boolean isPasswordCorrect(User user, String currentPassword) {
public boolean isPasswordCorrect(User user, String currentPassword) { return passwordEncoder.matches(currentPassword, user.getPassword());
return passwordEncoder.matches(currentPassword, user.getPassword()); }
} }
}

View File

@@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.CropPdfForm; import stirling.software.SPDF.model.api.general.CropPdfForm;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@@ -28,59 +29,62 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class CropController { public class CropController {
private static final Logger logger = LoggerFactory.getLogger(CropController.class); private static final Logger logger = LoggerFactory.getLogger(CropController.class);
@PostMapping(value = "/crop", consumes = "multipart/form-data") @PostMapping(value = "/crop", consumes = "multipart/form-data")
@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") @Operation(
public ResponseEntity<byte[]> cropPdf(@ModelAttribute CropPdfForm form) summary = "Crops a PDF document",
throws IOException { description =
"This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> cropPdf(@ModelAttribute CropPdfForm form) throws IOException {
PDDocument sourceDocument =
PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()));
PDDocument newDocument = new PDDocument();
PDDocument sourceDocument = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes())); int totalPages = sourceDocument.getNumberOfPages();
PDDocument newDocument = new PDDocument(); LayerUtility layerUtility = new LayerUtility(newDocument);
int totalPages = sourceDocument.getNumberOfPages(); for (int i = 0; i < totalPages; i++) {
PDPage sourcePage = sourceDocument.getPage(i);
LayerUtility layerUtility = new LayerUtility(newDocument); // Create a new page with the size of the source page
PDPage newPage = new PDPage(sourcePage.getMediaBox());
newDocument.addPage(newPage);
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage);
for (int i = 0; i < totalPages; i++) { // Import the source page as a form XObject
PDPage sourcePage = sourceDocument.getPage(i); PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);
// Create a new page with the size of the source page
PDPage newPage = new PDPage(sourcePage.getMediaBox());
newDocument.addPage(newPage);
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage);
// Import the source page as a form XObject contentStream.saveGraphicsState();
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);
contentStream.saveGraphicsState(); // Define the crop area
contentStream.addRect(form.getX(), form.getY(), form.getWidth(), form.getHeight());
// Define the crop area contentStream.clip();
contentStream.addRect(form.getX(), form.getY(), form.getWidth(), form.getHeight());
contentStream.clip();
// Draw the entire formXObject // Draw the entire formXObject
contentStream.drawForm(formXObject); contentStream.drawForm(formXObject);
contentStream.restoreGraphicsState(); contentStream.restoreGraphicsState();
contentStream.close();
// Now, set the new page's media box to the cropped size
newPage.setMediaBox(new PDRectangle(form.getX(), form.getY(), form.getWidth(), form.getHeight()));
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDocument.save(baos);
newDocument.close();
sourceDocument.close();
byte[] pdfContent = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(pdfContent, form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf");
}
contentStream.close();
// Now, set the new page's media box to the cropped size
newPage.setMediaBox(
new PDRectangle(form.getX(), form.getY(), form.getWidth(), form.getHeight()));
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDocument.save(baos);
newDocument.close();
sourceDocument.close();
byte[] pdfContent = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(
pdfContent,
form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_cropped.pdf");
}
} }

View File

@@ -1,7 +1,15 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import io.swagger.v3.oas.annotations.Operation; import java.io.ByteArrayInputStream;
import io.swagger.v3.oas.annotations.tags.Tag; import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import org.apache.pdfbox.io.MemoryUsageSetting; import org.apache.pdfbox.io.MemoryUsageSetting;
import org.apache.pdfbox.multipdf.PDFMergerUtility; import org.apache.pdfbox.multipdf.PDFMergerUtility;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
@@ -14,19 +22,13 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.MergePdfsRequest; import stirling.software.SPDF.model.api.general.MergePdfsRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
@@ -34,7 +36,6 @@ public class MergeController {
private static final Logger logger = LoggerFactory.getLogger(MergeController.class); private static final Logger logger = LoggerFactory.getLogger(MergeController.class);
private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException { private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
PDDocument mergedDoc = new PDDocument(); PDDocument mergedDoc = new PDDocument();
for (PDDocument doc : documents) { for (PDDocument doc : documents) {
@@ -52,27 +53,39 @@ public class MergeController {
case "byDateModified": case "byDateModified":
return (file1, file2) -> { return (file1, file2) -> {
try { try {
BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class); BasicFileAttributes attr1 =
BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class); Files.readAttributes(
Paths.get(file1.getOriginalFilename()),
BasicFileAttributes.class);
BasicFileAttributes attr2 =
Files.readAttributes(
Paths.get(file2.getOriginalFilename()),
BasicFileAttributes.class);
return attr1.lastModifiedTime().compareTo(attr2.lastModifiedTime()); return attr1.lastModifiedTime().compareTo(attr2.lastModifiedTime());
} catch (IOException e) { } catch (IOException e) {
return 0; // If there's an error, treat them as equal return 0; // If there's an error, treat them as equal
} }
}; };
case "byDateCreated": case "byDateCreated":
return (file1, file2) -> { return (file1, file2) -> {
try { try {
BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class); BasicFileAttributes attr1 =
BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class); Files.readAttributes(
Paths.get(file1.getOriginalFilename()),
BasicFileAttributes.class);
BasicFileAttributes attr2 =
Files.readAttributes(
Paths.get(file2.getOriginalFilename()),
BasicFileAttributes.class);
return attr1.creationTime().compareTo(attr2.creationTime()); return attr1.creationTime().compareTo(attr2.creationTime());
} catch (IOException e) { } catch (IOException e) {
return 0; // If there's an error, treat them as equal return 0; // If there's an error, treat them as equal
} }
}; };
case "byPDFTitle": case "byPDFTitle":
return (file1, file2) -> { return (file1, file2) -> {
try (PDDocument doc1 = PDDocument.load(file1.getInputStream()); try (PDDocument doc1 = PDDocument.load(file1.getInputStream());
PDDocument doc2 = PDDocument.load(file2.getInputStream())) { PDDocument doc2 = PDDocument.load(file2.getInputStream())) {
String title1 = doc1.getDocumentInformation().getTitle(); String title1 = doc1.getDocumentInformation().getTitle();
String title2 = doc2.getDocumentInformation().getTitle(); String title2 = doc2.getDocumentInformation().getTitle();
return title1.compareTo(title2); return title1.compareTo(title2);
@@ -82,14 +95,17 @@ public class MergeController {
}; };
case "orderProvided": case "orderProvided":
default: default:
return (file1, file2) -> 0; // Default is the order provided return (file1, file2) -> 0; // Default is the order provided
} }
} }
@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs") @PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
@Operation(summary = "Merge multiple PDF files into one", @Operation(
description = "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO") summary = "Merge multiple PDF files into one",
public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest form) throws IOException { description =
"This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO")
public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest form)
throws IOException {
try { try {
MultipartFile[] files = form.getFileInput(); MultipartFile[] files = form.getFileInput();
Arrays.sort(files, getSortComparator(form.getSortType())); Arrays.sort(files, getSortComparator(form.getSortType()));
@@ -101,14 +117,16 @@ public class MergeController {
mergedDoc.addSource(new ByteArrayInputStream(file.getBytes())); mergedDoc.addSource(new ByteArrayInputStream(file.getBytes()));
} }
mergedDoc.setDestinationFileName(files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf"); mergedDoc.setDestinationFileName(
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf");
mergedDoc.setDestinationStream(docOutputstream); mergedDoc.setDestinationStream(docOutputstream);
mergedDoc.mergeDocuments(MemoryUsageSetting.setupMainMemoryOnly()); mergedDoc.mergeDocuments(MemoryUsageSetting.setupMainMemoryOnly());
return WebResponseUtils.bytesToWebResponse(docOutputstream.toByteArray(), mergedDoc.getDestinationFileName()); return WebResponseUtils.bytesToWebResponse(
docOutputstream.toByteArray(), mergedDoc.getDestinationFileName());
} catch (Exception ex) { } catch (Exception ex) {
logger.error("Error in merge pdf process", ex); logger.error("Error in merge pdf process", ex);
throw ex; throw ex;
} }
} }
} }

View File

@@ -1,6 +1,5 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.awt.Color; import java.awt.Color;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
@@ -23,6 +22,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest; import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@@ -31,94 +31,110 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class MultiPageLayoutController { public class MultiPageLayoutController {
private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class); private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class);
@PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data") @PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data")
@Operation( @Operation(
summary = "Merge multiple pages of a PDF document into a single page", summary = "Merge multiple pages of a PDF document into a single page",
description = "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO" description =
) "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(@ModelAttribute MergeMultiplePagesRequest request) public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(
throws IOException { @ModelAttribute MergeMultiplePagesRequest request) throws IOException {
int pagesPerSheet = request.getPagesPerSheet(); int pagesPerSheet = request.getPagesPerSheet();
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
boolean addBorder = request.isAddBorder(); boolean addBorder = request.isAddBorder();
if (pagesPerSheet != 2 && pagesPerSheet != 3 && pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) {
throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square");
}
int cols = pagesPerSheet == 2 || pagesPerSheet == 3 ? pagesPerSheet : (int) Math.sqrt(pagesPerSheet); if (pagesPerSheet != 2
int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet); && pagesPerSheet != 3
&& pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) {
throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square");
}
PDDocument sourceDocument = PDDocument.load(file.getInputStream()); int cols =
PDDocument newDocument = new PDDocument(); pagesPerSheet == 2 || pagesPerSheet == 3
PDPage newPage = new PDPage(PDRectangle.A4); ? pagesPerSheet
newDocument.addPage(newPage); : (int) Math.sqrt(pagesPerSheet);
int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet);
int totalPages = sourceDocument.getNumberOfPages(); PDDocument sourceDocument = PDDocument.load(file.getInputStream());
float cellWidth = newPage.getMediaBox().getWidth() / cols; PDDocument newDocument = new PDDocument();
float cellHeight = newPage.getMediaBox().getHeight() / rows; PDPage newPage = new PDPage(PDRectangle.A4);
newDocument.addPage(newPage);
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true); int totalPages = sourceDocument.getNumberOfPages();
LayerUtility layerUtility = new LayerUtility(newDocument); float cellWidth = newPage.getMediaBox().getWidth() / cols;
float cellHeight = newPage.getMediaBox().getHeight() / rows;
float borderThickness = 1.5f; // Specify border thickness as required PDPageContentStream contentStream =
contentStream.setLineWidth(borderThickness); new PDPageContentStream(
contentStream.setStrokingColor(Color.BLACK); newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
LayerUtility layerUtility = new LayerUtility(newDocument);
for (int i = 0; i < totalPages; i++) {
if (i != 0 && i % pagesPerSheet == 0) {
// Close the current content stream and create a new page and content stream
contentStream.close();
newPage = new PDPage(PDRectangle.A4);
newDocument.addPage(newPage);
contentStream = new PDPageContentStream(newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
}
PDPage sourcePage = sourceDocument.getPage(i); float borderThickness = 1.5f; // Specify border thickness as required
PDRectangle rect = sourcePage.getMediaBox(); contentStream.setLineWidth(borderThickness);
float scaleWidth = cellWidth / rect.getWidth(); contentStream.setStrokingColor(Color.BLACK);
float scaleHeight = cellHeight / rect.getHeight();
float scale = Math.min(scaleWidth, scaleHeight);
int adjustedPageIndex = i % pagesPerSheet; // This will reset the index for every new page for (int i = 0; i < totalPages; i++) {
int rowIndex = adjustedPageIndex / cols; if (i != 0 && i % pagesPerSheet == 0) {
int colIndex = adjustedPageIndex % cols; // Close the current content stream and create a new page and content stream
contentStream.close();
newPage = new PDPage(PDRectangle.A4);
newDocument.addPage(newPage);
contentStream =
new PDPageContentStream(
newDocument,
newPage,
PDPageContentStream.AppendMode.APPEND,
true,
true);
}
float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2; PDPage sourcePage = sourceDocument.getPage(i);
float y = newPage.getMediaBox().getHeight() - ((rowIndex + 1) * cellHeight - (cellHeight - rect.getHeight() * scale) / 2); PDRectangle rect = sourcePage.getMediaBox();
float scaleWidth = cellWidth / rect.getWidth();
float scaleHeight = cellHeight / rect.getHeight();
float scale = Math.min(scaleWidth, scaleHeight);
contentStream.saveGraphicsState(); int adjustedPageIndex =
contentStream.transform(Matrix.getTranslateInstance(x, y)); i % pagesPerSheet; // This will reset the index for every new page
contentStream.transform(Matrix.getScaleInstance(scale, scale)); int rowIndex = adjustedPageIndex / cols;
int colIndex = adjustedPageIndex % cols;
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i); float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2;
contentStream.drawForm(formXObject); float y =
newPage.getMediaBox().getHeight()
- ((rowIndex + 1) * cellHeight
- (cellHeight - rect.getHeight() * scale) / 2);
contentStream.restoreGraphicsState(); contentStream.saveGraphicsState();
contentStream.transform(Matrix.getTranslateInstance(x, y));
if(addBorder) { contentStream.transform(Matrix.getScaleInstance(scale, scale));
// Draw border around each page
float borderX = colIndex * cellWidth;
float borderY = newPage.getMediaBox().getHeight() - (rowIndex + 1) * cellHeight;
contentStream.addRect(borderX, borderY, cellWidth, cellHeight);
contentStream.stroke();
}
}
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);
contentStream.drawForm(formXObject);
contentStream.close(); // Close the final content stream contentStream.restoreGraphicsState();
sourceDocument.close();
ByteArrayOutputStream baos = new ByteArrayOutputStream(); if (addBorder) {
newDocument.save(baos); // Draw border around each page
newDocument.close(); float borderX = colIndex * cellWidth;
float borderY = newPage.getMediaBox().getHeight() - (rowIndex + 1) * cellHeight;
contentStream.addRect(borderX, borderY, cellWidth, cellHeight);
contentStream.stroke();
}
}
byte[] result = baos.toByteArray(); contentStream.close(); // Close the final content stream
return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf"); sourceDocument.close();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDocument.save(baos);
newDocument.close();
byte[] result = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(
result,
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf");
}
} }

View File

@@ -1,11 +1,13 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.ArrayList;
import org.apache.pdfbox.multipdf.Overlay; import org.apache.pdfbox.multipdf.Overlay;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -18,36 +20,49 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.OverlayPdfsRequest; import stirling.software.SPDF.model.api.general.OverlayPdfsRequest;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class PdfOverlayController { public class PdfOverlayController {
@PostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data") @PostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data")
@Operation(summary = "Overlay PDF files in various modes", description = "Overlay PDF files onto a base PDF with different modes: Sequential, Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO") @Operation(
public ResponseEntity<byte[]> overlayPdfs(@ModelAttribute OverlayPdfsRequest request) throws IOException { summary = "Overlay PDF files in various modes",
description =
"Overlay PDF files onto a base PDF with different modes: Sequential, Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO")
public ResponseEntity<byte[]> overlayPdfs(@ModelAttribute OverlayPdfsRequest request)
throws IOException {
MultipartFile baseFile = request.getFileInput(); MultipartFile baseFile = request.getFileInput();
int overlayPos = request.getOverlayPosition(); int overlayPos = request.getOverlayPosition();
MultipartFile[] overlayFiles = request.getOverlayFiles(); MultipartFile[] overlayFiles = request.getOverlayFiles();
File[] overlayPdfFiles = new File[overlayFiles.length]; File[] overlayPdfFiles = new File[overlayFiles.length];
List<File> tempFiles = new ArrayList<>(); // List to keep track of temporary files List<File> tempFiles = new ArrayList<>(); // List to keep track of temporary files
try { try {
for (int i = 0; i < overlayFiles.length; i++) { for (int i = 0; i < overlayFiles.length; i++) {
overlayPdfFiles[i] = GeneralUtils.multipartToFile(overlayFiles[i]); overlayPdfFiles[i] = GeneralUtils.multipartToFile(overlayFiles[i]);
} }
String mode = request.getOverlayMode(); // "SequentialOverlay", "InterleavedOverlay", "FixedRepeatOverlay" String mode = request.getOverlayMode(); // "SequentialOverlay", "InterleavedOverlay",
// "FixedRepeatOverlay"
int[] counts = request.getCounts(); // Used for FixedRepeatOverlay mode int[] counts = request.getCounts(); // Used for FixedRepeatOverlay mode
try (PDDocument basePdf = PDDocument.load(baseFile.getInputStream()); try (PDDocument basePdf = PDDocument.load(baseFile.getInputStream());
Overlay overlay = new Overlay()) { Overlay overlay = new Overlay()) {
Map<Integer, String> overlayGuide = prepareOverlayGuide(basePdf.getNumberOfPages(), overlayPdfFiles, mode, counts, tempFiles); Map<Integer, String> overlayGuide =
prepareOverlayGuide(
basePdf.getNumberOfPages(),
overlayPdfFiles,
mode,
counts,
tempFiles);
overlay.setInputPDF(basePdf); overlay.setInputPDF(basePdf);
if (overlayPos == 0) { if (overlayPos == 0) {
overlay.setOverlayPosition(Overlay.Position.FOREGROUND); overlay.setOverlayPosition(Overlay.Position.FOREGROUND);
@@ -58,10 +73,13 @@ public class PdfOverlayController {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
overlay.overlay(overlayGuide).save(outputStream); overlay.overlay(overlayGuide).save(outputStream);
byte[] data = outputStream.toByteArray(); byte[] data = outputStream.toByteArray();
String outputFilename = baseFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf"; // Remove file extension and append .pdf String outputFilename =
baseFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
return WebResponseUtils.bytesToWebResponse(data, outputFilename, MediaType.APPLICATION_PDF); + "_overlayed.pdf"; // Remove file extension and append .pdf
}
return WebResponseUtils.bytesToWebResponse(
data, outputFilename, MediaType.APPLICATION_PDF);
}
} finally { } finally {
for (File overlayPdfFile : overlayPdfFiles) { for (File overlayPdfFile : overlayPdfFiles) {
if (overlayPdfFile != null) { if (overlayPdfFile != null) {
@@ -76,7 +94,9 @@ public class PdfOverlayController {
} }
} }
private Map<Integer, String> prepareOverlayGuide(int basePageCount, File[] overlayFiles, String mode, int[] counts, List<File> tempFiles) throws IOException { private Map<Integer, String> prepareOverlayGuide(
int basePageCount, File[] overlayFiles, String mode, int[] counts, List<File> tempFiles)
throws IOException {
Map<Integer, String> overlayGuide = new HashMap<>(); Map<Integer, String> overlayGuide = new HashMap<>();
switch (mode) { switch (mode) {
case "SequentialOverlay": case "SequentialOverlay":
@@ -94,12 +114,19 @@ public class PdfOverlayController {
return overlayGuide; return overlayGuide;
} }
private void sequentialOverlay(Map<Integer, String> overlayGuide, File[] overlayFiles, int basePageCount, List<File> tempFiles) throws IOException { private void sequentialOverlay(
Map<Integer, String> overlayGuide,
File[] overlayFiles,
int basePageCount,
List<File> tempFiles)
throws IOException {
int overlayFileIndex = 0; int overlayFileIndex = 0;
int pageCountInCurrentOverlay = 0; int pageCountInCurrentOverlay = 0;
for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) { for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) {
if (pageCountInCurrentOverlay == 0 || pageCountInCurrentOverlay >= getNumberOfPages(overlayFiles[overlayFileIndex])) { if (pageCountInCurrentOverlay == 0
|| pageCountInCurrentOverlay
>= getNumberOfPages(overlayFiles[overlayFileIndex])) {
pageCountInCurrentOverlay = 0; pageCountInCurrentOverlay = 0;
overlayFileIndex = (overlayFileIndex + 1) % overlayFiles.length; overlayFileIndex = (overlayFileIndex + 1) % overlayFiles.length;
} }
@@ -125,13 +152,9 @@ public class PdfOverlayController {
} }
} }
private void interleavedOverlay(
Map<Integer, String> overlayGuide, File[] overlayFiles, int basePageCount)
throws IOException {
private void interleavedOverlay(Map<Integer, String> overlayGuide, File[] overlayFiles, int basePageCount) throws IOException {
for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) { for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) {
File overlayFile = overlayFiles[(basePageIndex - 1) % overlayFiles.length]; File overlayFile = overlayFiles[(basePageIndex - 1) % overlayFiles.length];
@@ -145,10 +168,12 @@ public class PdfOverlayController {
} }
} }
private void fixedRepeatOverlay(
private void fixedRepeatOverlay(Map<Integer, String> overlayGuide, File[] overlayFiles, int[] counts, int basePageCount) throws IOException { Map<Integer, String> overlayGuide, File[] overlayFiles, int[] counts, int basePageCount)
throws IOException {
if (overlayFiles.length != counts.length) { if (overlayFiles.length != counts.length) {
throw new IllegalArgumentException("Counts array length must match the number of overlay files"); throw new IllegalArgumentException(
"Counts array length must match the number of overlay files");
} }
int currentPage = 1; int currentPage = 1;
for (int i = 0; i < overlayFiles.length; i++) { for (int i = 0; i < overlayFiles.length; i++) {
@@ -167,7 +192,7 @@ public class PdfOverlayController {
} }
} }
} }
} }
// Additional classes like OverlayPdfsRequest, WebResponseUtils, etc. are assumed to be defined elsewhere. // Additional classes like OverlayPdfsRequest, WebResponseUtils, etc. are assumed to be defined
// elsewhere.

View File

@@ -17,200 +17,204 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.SortTypes; import stirling.software.SPDF.model.SortTypes;
import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.model.api.general.RearrangePagesRequest; import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class RearrangePagesPDFController { public class RearrangePagesPDFController {
private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class); private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class);
@PostMapping(consumes = "multipart/form-data", value = "/remove-pages") @PostMapping(consumes = "multipart/form-data", value = "/remove-pages")
@Operation(summary = "Remove pages from a PDF file", description = "This endpoint removes specified pages from a given PDF file. Users can provide a comma-separated list of page numbers or ranges to delete. Input:PDF Output:PDF Type:SISO") @Operation(
public ResponseEntity<byte[]> deletePages(@ModelAttribute PDFWithPageNums request ) summary = "Remove pages from a PDF file",
throws IOException { description =
"This endpoint removes specified pages from a given PDF file. Users can provide a comma-separated list of page numbers or ranges to delete. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> deletePages(@ModelAttribute PDFWithPageNums request)
throws IOException {
MultipartFile pdfFile = request.getFileInput(); MultipartFile pdfFile = request.getFileInput();
String pagesToDelete = request.getPageNumbers(); String pagesToDelete = request.getPageNumbers();
PDDocument document = PDDocument.load(pdfFile.getBytes());
// Split the page order string into an array of page numbers or range of numbers PDDocument document = PDDocument.load(pdfFile.getBytes());
String[] pageOrderArr = pagesToDelete.split(",");
List<Integer> pagesToRemove = GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); // Split the page order string into an array of page numbers or range of numbers
String[] pageOrderArr = pagesToDelete.split(",");
for (int i = pagesToRemove.size() - 1; i >= 0; i--) { List<Integer> pagesToRemove =
int pageIndex = pagesToRemove.get(i); GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages());
document.removePage(pageIndex);
}
return WebResponseUtils.pdfDocToWebResponse(document,
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf");
} for (int i = pagesToRemove.size() - 1; i >= 0; i--) {
int pageIndex = pagesToRemove.get(i);
document.removePage(pageIndex);
}
private List<Integer> removeFirst(int totalPages) { return WebResponseUtils.pdfDocToWebResponse(
if (totalPages <= 1) document,
return new ArrayList<>(); pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf");
List<Integer> newPageOrder = new ArrayList<>();
for (int i = 2; i <= totalPages; i++) {
newPageOrder.add(i - 1);
}
return newPageOrder;
}
private List<Integer> removeLast(int totalPages) {
if (totalPages <= 1)
return new ArrayList<>();
List<Integer> newPageOrder = new ArrayList<>();
for (int i = 1; i < totalPages; i++) {
newPageOrder.add(i - 1);
}
return newPageOrder;
}
private List<Integer> removeFirstAndLast(int totalPages) {
if (totalPages <= 2)
return new ArrayList<>();
List<Integer> newPageOrder = new ArrayList<>();
for (int i = 2; i < totalPages; i++) {
newPageOrder.add(i - 1);
}
return newPageOrder;
}
private List<Integer> reverseOrder(int totalPages) {
List<Integer> newPageOrder = new ArrayList<>();
for (int i = totalPages; i >= 1; i--) {
newPageOrder.add(i - 1);
}
return newPageOrder;
}
private List<Integer> duplexSort(int totalPages) {
List<Integer> newPageOrder = new ArrayList<>();
int half = (totalPages + 1) / 2; // This ensures proper behavior with odd numbers of pages
for (int i = 1; i <= half; i++) {
newPageOrder.add(i - 1);
if (i <= totalPages - half) { // Avoid going out of bounds
newPageOrder.add(totalPages - i);
}
}
return newPageOrder;
}
private List<Integer> bookletSort(int totalPages) {
List<Integer> newPageOrder = new ArrayList<>();
for (int i = 0; i < totalPages / 2; i++) {
newPageOrder.add(i);
newPageOrder.add(totalPages - i - 1);
}
return newPageOrder;
}
private List<Integer> sideStitchBooklet(int totalPages) {
List<Integer> newPageOrder = new ArrayList<>();
for (int i = 0; i < (totalPages + 3) / 4; i++) {
int begin = i * 4;
newPageOrder.add(Math.min(begin + 3, totalPages - 1));
newPageOrder.add(Math.min(begin, totalPages - 1));
newPageOrder.add(Math.min(begin + 1, totalPages - 1));
newPageOrder.add(Math.min(begin + 2, totalPages - 1));
}
return newPageOrder;
} }
private List<Integer> oddEvenSplit(int totalPages) { private List<Integer> removeFirst(int totalPages) {
List<Integer> newPageOrder = new ArrayList<>(); if (totalPages <= 1) return new ArrayList<>();
for (int i = 1; i <= totalPages; i += 2) { List<Integer> newPageOrder = new ArrayList<>();
newPageOrder.add(i - 1); for (int i = 2; i <= totalPages; i++) {
} newPageOrder.add(i - 1);
for (int i = 2; i <= totalPages; i += 2) { }
newPageOrder.add(i - 1); return newPageOrder;
} }
return newPageOrder;
}
private List<Integer> processSortTypes(String sortTypes, int totalPages) { private List<Integer> removeLast(int totalPages) {
try { if (totalPages <= 1) return new ArrayList<>();
SortTypes mode = SortTypes.valueOf(sortTypes.toUpperCase()); List<Integer> newPageOrder = new ArrayList<>();
switch (mode) { for (int i = 1; i < totalPages; i++) {
case REVERSE_ORDER: newPageOrder.add(i - 1);
return reverseOrder(totalPages); }
case DUPLEX_SORT: return newPageOrder;
return duplexSort(totalPages); }
case BOOKLET_SORT:
return bookletSort(totalPages);
case SIDE_STITCH_BOOKLET_SORT:
return sideStitchBooklet(totalPages);
case ODD_EVEN_SPLIT:
return oddEvenSplit(totalPages);
case REMOVE_FIRST:
return removeFirst(totalPages);
case REMOVE_LAST:
return removeLast(totalPages);
case REMOVE_FIRST_AND_LAST:
return removeFirstAndLast(totalPages);
default:
throw new IllegalArgumentException("Unsupported custom mode");
}
} catch (IllegalArgumentException e) {
logger.error("Unsupported custom mode", e);
return null;
}
}
@PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages") private List<Integer> removeFirstAndLast(int totalPages) {
@Operation(summary = "Rearrange pages in a PDF file", description = "This endpoint rearranges pages in a given PDF file based on the specified page order or custom mode. Users can provide a page order as a comma-separated list of page numbers or page ranges, or a custom mode. Input:PDF Output:PDF") if (totalPages <= 2) return new ArrayList<>();
public ResponseEntity<byte[]> rearrangePages(@ModelAttribute RearrangePagesRequest request) throws IOException { List<Integer> newPageOrder = new ArrayList<>();
MultipartFile pdfFile = request.getFileInput(); for (int i = 2; i < totalPages; i++) {
String pageOrder = request.getPageNumbers(); newPageOrder.add(i - 1);
String sortType = request.getCustomMode(); }
try { return newPageOrder;
// Load the input PDF }
PDDocument document = PDDocument.load(pdfFile.getInputStream());
// Split the page order string into an array of page numbers or range of numbers private List<Integer> reverseOrder(int totalPages) {
String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0]; List<Integer> newPageOrder = new ArrayList<>();
int totalPages = document.getNumberOfPages(); for (int i = totalPages; i >= 1; i--) {
List<Integer> newPageOrder; newPageOrder.add(i - 1);
if (sortType != null && sortType.length() > 0) { }
newPageOrder = processSortTypes(sortType, totalPages); return newPageOrder;
} else { }
newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages);
}
logger.info("newPageOrder = " +newPageOrder);
logger.info("totalPages = " +totalPages);
// Create a new list to hold the pages in the new order
List<PDPage> newPages = new ArrayList<>();
for (int i = 0; i < newPageOrder.size(); i++) {
newPages.add(document.getPage(newPageOrder.get(i)));
}
// Remove all the pages from the original document private List<Integer> duplexSort(int totalPages) {
for (int i = document.getNumberOfPages() - 1; i >= 0; i--) { List<Integer> newPageOrder = new ArrayList<>();
document.removePage(i); int half = (totalPages + 1) / 2; // This ensures proper behavior with odd numbers of pages
} for (int i = 1; i <= half; i++) {
newPageOrder.add(i - 1);
if (i <= totalPages - half) { // Avoid going out of bounds
newPageOrder.add(totalPages - i);
}
}
return newPageOrder;
}
// Add the pages in the new order private List<Integer> bookletSort(int totalPages) {
for (PDPage page : newPages) { List<Integer> newPageOrder = new ArrayList<>();
document.addPage(page); for (int i = 0; i < totalPages / 2; i++) {
} newPageOrder.add(i);
newPageOrder.add(totalPages - i - 1);
}
return newPageOrder;
}
return WebResponseUtils.pdfDocToWebResponse(document, private List<Integer> sideStitchBooklet(int totalPages) {
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rearranged.pdf"); List<Integer> newPageOrder = new ArrayList<>();
} catch (IOException e) { for (int i = 0; i < (totalPages + 3) / 4; i++) {
logger.error("Failed rearranging documents", e); int begin = i * 4;
return null; newPageOrder.add(Math.min(begin + 3, totalPages - 1));
} newPageOrder.add(Math.min(begin, totalPages - 1));
} newPageOrder.add(Math.min(begin + 1, totalPages - 1));
newPageOrder.add(Math.min(begin + 2, totalPages - 1));
}
return newPageOrder;
}
private List<Integer> oddEvenSplit(int totalPages) {
List<Integer> newPageOrder = new ArrayList<>();
for (int i = 1; i <= totalPages; i += 2) {
newPageOrder.add(i - 1);
}
for (int i = 2; i <= totalPages; i += 2) {
newPageOrder.add(i - 1);
}
return newPageOrder;
}
private List<Integer> processSortTypes(String sortTypes, int totalPages) {
try {
SortTypes mode = SortTypes.valueOf(sortTypes.toUpperCase());
switch (mode) {
case REVERSE_ORDER:
return reverseOrder(totalPages);
case DUPLEX_SORT:
return duplexSort(totalPages);
case BOOKLET_SORT:
return bookletSort(totalPages);
case SIDE_STITCH_BOOKLET_SORT:
return sideStitchBooklet(totalPages);
case ODD_EVEN_SPLIT:
return oddEvenSplit(totalPages);
case REMOVE_FIRST:
return removeFirst(totalPages);
case REMOVE_LAST:
return removeLast(totalPages);
case REMOVE_FIRST_AND_LAST:
return removeFirstAndLast(totalPages);
default:
throw new IllegalArgumentException("Unsupported custom mode");
}
} catch (IllegalArgumentException e) {
logger.error("Unsupported custom mode", e);
return null;
}
}
@PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages")
@Operation(
summary = "Rearrange pages in a PDF file",
description =
"This endpoint rearranges pages in a given PDF file based on the specified page order or custom mode. Users can provide a page order as a comma-separated list of page numbers or page ranges, or a custom mode. Input:PDF Output:PDF")
public ResponseEntity<byte[]> rearrangePages(@ModelAttribute RearrangePagesRequest request)
throws IOException {
MultipartFile pdfFile = request.getFileInput();
String pageOrder = request.getPageNumbers();
String sortType = request.getCustomMode();
try {
// Load the input PDF
PDDocument document = PDDocument.load(pdfFile.getInputStream());
// Split the page order string into an array of page numbers or range of numbers
String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0];
int totalPages = document.getNumberOfPages();
List<Integer> newPageOrder;
if (sortType != null && sortType.length() > 0) {
newPageOrder = processSortTypes(sortType, totalPages);
} else {
newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages);
}
logger.info("newPageOrder = " + newPageOrder);
logger.info("totalPages = " + totalPages);
// Create a new list to hold the pages in the new order
List<PDPage> newPages = new ArrayList<>();
for (int i = 0; i < newPageOrder.size(); i++) {
newPages.add(document.getPage(newPageOrder.get(i)));
}
// Remove all the pages from the original document
for (int i = document.getNumberOfPages() - 1; i >= 0; i--) {
document.removePage(i);
}
// Add the pages in the new order
for (PDPage page : newPages) {
document.addPage(page);
}
return WebResponseUtils.pdfDocToWebResponse(
document,
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_rearranged.pdf");
} catch (IOException e) {
logger.error("Failed rearranging documents", e);
return null;
}
}
} }

View File

@@ -16,6 +16,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.RotatePDFRequest; import stirling.software.SPDF.model.api.general.RotatePDFRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@@ -28,11 +29,11 @@ public class RotationController {
@PostMapping(consumes = "multipart/form-data", value = "/rotate-pdf") @PostMapping(consumes = "multipart/form-data", value = "/rotate-pdf")
@Operation( @Operation(
summary = "Rotate a PDF file", summary = "Rotate a PDF file",
description = "This endpoint rotates a given PDF file by a specified angle. The angle must be a multiple of 90. Input:PDF Output:PDF Type:SISO" description =
) "This endpoint rotates a given PDF file by a specified angle. The angle must be a multiple of 90. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> rotatePDF( public ResponseEntity<byte[]> rotatePDF(@ModelAttribute RotatePDFRequest request)
@ModelAttribute RotatePDFRequest request) throws IOException { throws IOException {
MultipartFile pdfFile = request.getFileInput(); MultipartFile pdfFile = request.getFileInput();
Integer angle = request.getAngle(); Integer angle = request.getAngle();
// Load the PDF document // Load the PDF document
@@ -45,8 +46,8 @@ public class RotationController {
page.setRotation(page.getRotation() + angle); page.setRotation(page.getRotation() + angle);
} }
return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rotated.pdf"); return WebResponseUtils.pdfDocToWebResponse(
document,
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rotated.pdf");
} }
} }

View File

@@ -23,88 +23,90 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.ScalePagesRequest; import stirling.software.SPDF.model.api.general.ScalePagesRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class ScalePagesController { public class ScalePagesController {
private static final Logger logger = LoggerFactory.getLogger(ScalePagesController.class); private static final Logger logger = LoggerFactory.getLogger(ScalePagesController.class);
@PostMapping(value = "/scale-pages", consumes = "multipart/form-data") @PostMapping(value = "/scale-pages", consumes = "multipart/form-data")
@Operation(summary = "Change the size of a PDF page/document", description = "This operation takes an input PDF file and the size to scale the pages to in the output PDF file. Input:PDF Output:PDF Type:SISO") @Operation(
public ResponseEntity<byte[]> scalePages(@ModelAttribute ScalePagesRequest request) throws IOException { summary = "Change the size of a PDF page/document",
MultipartFile file = request.getFileInput(); description =
String targetPDRectangle = request.getPageSize(); "This operation takes an input PDF file and the size to scale the pages to in the output PDF file. Input:PDF Output:PDF Type:SISO")
float scaleFactor = request.getScaleFactor(); public ResponseEntity<byte[]> scalePages(@ModelAttribute ScalePagesRequest request)
throws IOException {
MultipartFile file = request.getFileInput();
String targetPDRectangle = request.getPageSize();
float scaleFactor = request.getScaleFactor();
Map<String, PDRectangle> sizeMap = new HashMap<>(); Map<String, PDRectangle> sizeMap = new HashMap<>();
// Add A0 - A10 // Add A0 - A10
sizeMap.put("A0", PDRectangle.A0); sizeMap.put("A0", PDRectangle.A0);
sizeMap.put("A1", PDRectangle.A1); sizeMap.put("A1", PDRectangle.A1);
sizeMap.put("A2", PDRectangle.A2); sizeMap.put("A2", PDRectangle.A2);
sizeMap.put("A3", PDRectangle.A3); sizeMap.put("A3", PDRectangle.A3);
sizeMap.put("A4", PDRectangle.A4); sizeMap.put("A4", PDRectangle.A4);
sizeMap.put("A5", PDRectangle.A5); sizeMap.put("A5", PDRectangle.A5);
sizeMap.put("A6", PDRectangle.A6); sizeMap.put("A6", PDRectangle.A6);
// Add other sizes // Add other sizes
sizeMap.put("LETTER", PDRectangle.LETTER); sizeMap.put("LETTER", PDRectangle.LETTER);
sizeMap.put("LEGAL", PDRectangle.LEGAL); sizeMap.put("LEGAL", PDRectangle.LEGAL);
if (!sizeMap.containsKey(targetPDRectangle)) { if (!sizeMap.containsKey(targetPDRectangle)) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Invalid PDRectangle. It must be one of the following: A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10"); "Invalid PDRectangle. It must be one of the following: A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10");
} }
PDRectangle targetSize = sizeMap.get(targetPDRectangle); PDRectangle targetSize = sizeMap.get(targetPDRectangle);
PDDocument sourceDocument = PDDocument.load(file.getBytes()); PDDocument sourceDocument = PDDocument.load(file.getBytes());
PDDocument outputDocument = new PDDocument(); PDDocument outputDocument = new PDDocument();
int totalPages = sourceDocument.getNumberOfPages(); int totalPages = sourceDocument.getNumberOfPages();
for (int i = 0; i < totalPages; i++) { for (int i = 0; i < totalPages; i++) {
PDPage sourcePage = sourceDocument.getPage(i); PDPage sourcePage = sourceDocument.getPage(i);
PDRectangle sourceSize = sourcePage.getMediaBox(); PDRectangle sourceSize = sourcePage.getMediaBox();
float scaleWidth = targetSize.getWidth() / sourceSize.getWidth();
float scaleHeight = targetSize.getHeight() / sourceSize.getHeight();
float scale = Math.min(scaleWidth, scaleHeight) * scaleFactor;
PDPage newPage = new PDPage(targetSize);
outputDocument.addPage(newPage);
PDPageContentStream contentStream = new PDPageContentStream(outputDocument, newPage, PDPageContentStream.AppendMode.APPEND, true);
float x = (targetSize.getWidth() - sourceSize.getWidth() * scale) / 2;
float y = (targetSize.getHeight() - sourceSize.getHeight() * scale) / 2;
contentStream.saveGraphicsState();
contentStream.transform(Matrix.getTranslateInstance(x, y));
contentStream.transform(Matrix.getScaleInstance(scale, scale));
LayerUtility layerUtility = new LayerUtility(outputDocument);
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, i);
contentStream.drawForm(form);
contentStream.restoreGraphicsState(); float scaleWidth = targetSize.getWidth() / sourceSize.getWidth();
contentStream.close(); float scaleHeight = targetSize.getHeight() / sourceSize.getHeight();
} float scale = Math.min(scaleWidth, scaleHeight) * scaleFactor;
PDPage newPage = new PDPage(targetSize);
outputDocument.addPage(newPage);
PDPageContentStream contentStream =
new PDPageContentStream(
outputDocument, newPage, PDPageContentStream.AppendMode.APPEND, true);
float x = (targetSize.getWidth() - sourceSize.getWidth() * scale) / 2;
float y = (targetSize.getHeight() - sourceSize.getHeight() * scale) / 2;
contentStream.saveGraphicsState();
contentStream.transform(Matrix.getTranslateInstance(x, y));
contentStream.transform(Matrix.getScaleInstance(scale, scale));
ByteArrayOutputStream baos = new ByteArrayOutputStream(); LayerUtility layerUtility = new LayerUtility(outputDocument);
outputDocument.save(baos); PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, i);
outputDocument.close(); contentStream.drawForm(form);
sourceDocument.close();
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(),
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf");
}
contentStream.restoreGraphicsState();
contentStream.close();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
outputDocument.save(baos);
outputDocument.close();
sourceDocument.close();
return WebResponseUtils.bytesToWebResponse(
baos.toByteArray(),
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf");
}
} }

View File

@@ -25,6 +25,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@@ -36,19 +37,24 @@ public class SplitPDFController {
private static final Logger logger = LoggerFactory.getLogger(SplitPDFController.class); private static final Logger logger = LoggerFactory.getLogger(SplitPDFController.class);
@PostMapping(consumes = "multipart/form-data", value = "/split-pages") @PostMapping(consumes = "multipart/form-data", value = "/split-pages")
@Operation(summary = "Split a PDF file into separate documents", @Operation(
description = "This endpoint splits a given PDF file into separate documents based on the specified page numbers or ranges. Users can specify pages using individual numbers, ranges, or 'all' for every page. Input:PDF Output:PDF Type:SIMO") summary = "Split a PDF file into separate documents",
public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request) throws IOException { description =
MultipartFile file = request.getFileInput(); "This endpoint splits a given PDF file into separate documents based on the specified page numbers or ranges. Users can specify pages using individual numbers, ranges, or 'all' for every page. Input:PDF Output:PDF Type:SIMO")
public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request)
throws IOException {
MultipartFile file = request.getFileInput();
String pages = request.getPageNumbers(); String pages = request.getPageNumbers();
// open the pdf document // open the pdf document
InputStream inputStream = file.getInputStream(); InputStream inputStream = file.getInputStream();
PDDocument document = PDDocument.load(inputStream); PDDocument document = PDDocument.load(inputStream);
List<Integer> pageNumbers = request.getPageNumbersList(document); List<Integer> pageNumbers = request.getPageNumbersList(document);
if(!pageNumbers.contains(document.getNumberOfPages() - 1)) if (!pageNumbers.contains(document.getNumberOfPages() - 1))
pageNumbers.add(document.getNumberOfPages()- 1); pageNumbers.add(document.getNumberOfPages() - 1);
logger.info("Splitting PDF into pages: {}", pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(","))); logger.info(
"Splitting PDF into pages: {}",
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
// split the document // split the document
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>(); List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
@@ -72,7 +78,6 @@ public class SplitPDFController {
} }
} }
// closing the original document // closing the original document
document.close(); document.close();
@@ -104,8 +109,7 @@ public class SplitPDFController {
Files.delete(zipFile); Files.delete(zipFile);
// return the Resource in the response // return the Resource in the response
return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} }
}
}

View File

@@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@@ -25,17 +26,22 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest; import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class SplitPdfBySectionsController { public class SplitPdfBySectionsController {
@PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data")
@PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data") @Operation(
@Operation(summary = "Split PDF pages into smaller sections", description = "Split each page of a PDF into smaller sections based on the user's choice (halves, thirds, quarters, etc.), both vertically and horizontally. Input:PDF Output:ZIP-PDF Type:SISO") summary = "Split PDF pages into smaller sections",
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfBySectionsRequest request) throws Exception { description =
"Split each page of a PDF into smaller sections based on the user's choice (halves, thirds, quarters, etc.), both vertically and horizontally. Input:PDF Output:ZIP-PDF Type:SISO")
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfBySectionsRequest request)
throws Exception {
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>(); List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
@@ -59,8 +65,6 @@ public class SplitPdfBySectionsController {
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", ""); String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
byte[] data; byte[] data;
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) { try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
int pageNum = 1; int pageNum = 1;
for (int i = 0; i < splitDocumentsBoas.size(); i++) { for (int i = 0; i < splitDocumentsBoas.size(); i++) {
@@ -82,10 +86,13 @@ public class SplitPdfBySectionsController {
Files.delete(zipFile); Files.delete(zipFile);
} }
return WebResponseUtils.bytesToWebResponse(data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.bytesToWebResponse(
data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM);
} }
public List<PDDocument> splitPdfPages(PDDocument document, int horizontalDivisions, int verticalDivisions) throws IOException { public List<PDDocument> splitPdfPages(
PDDocument document, int horizontalDivisions, int verticalDivisions)
throws IOException {
List<PDDocument> splitDocuments = new ArrayList<>(); List<PDDocument> splitDocuments = new ArrayList<>();
for (PDPage originalPage : document.getPages()) { for (PDPage originalPage : document.getPages()) {
@@ -103,13 +110,19 @@ public class SplitPdfBySectionsController {
PDPage subPage = new PDPage(new PDRectangle(subPageWidth, subPageHeight)); PDPage subPage = new PDPage(new PDRectangle(subPageWidth, subPageHeight));
subDoc.addPage(subPage); subDoc.addPage(subPage);
PDFormXObject form = layerUtility.importPageAsForm(document, document.getPages().indexOf(originalPage)); PDFormXObject form =
layerUtility.importPageAsForm(
document, document.getPages().indexOf(originalPage));
try (PDPageContentStream contentStream = new PDPageContentStream(subDoc, subPage)) { try (PDPageContentStream contentStream =
new PDPageContentStream(subDoc, subPage)) {
// Set clipping area and position // Set clipping area and position
float translateX = -subPageWidth * i; float translateX = -subPageWidth * i;
float translateY = height - subPageHeight * (verticalDivisions - j); float translateY = height - subPageHeight * (verticalDivisions - j);
// Code for google Docs pdfs..
// float translateY = -subPageHeight * (verticalDivisions - 1 - j);
contentStream.saveGraphicsState(); contentStream.saveGraphicsState();
contentStream.addRect(0, 0, subPageWidth, subPageHeight); contentStream.addRect(0, 0, subPageWidth, subPageHeight);
contentStream.clip(); contentStream.clip();
@@ -127,9 +140,4 @@ public class SplitPdfBySectionsController {
return splitDocuments; return splitDocuments;
} }
} }

View File

@@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@@ -20,6 +21,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest; import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@@ -29,22 +31,23 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class SplitPdfBySizeController { public class SplitPdfBySizeController {
@PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data") @PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data")
@Operation(summary = "Auto split PDF pages into separate documents based on size or count", description = "split PDF into multiple paged documents based on size/count, ie if 20 pages and split into 5, it does 5 documents each 4 pages\r\n" @Operation(
+ " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB (rounded so that it accepts 1.9MB but not 2.1MB) Input:PDF Output:ZIP-PDF Type:SISO") summary = "Auto split PDF pages into separate documents based on size or count",
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request) throws Exception { description =
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<ByteArrayOutputStream>(); "split PDF into multiple paged documents based on size/count, ie if 20 pages and split into 5, it does 5 documents each 4 pages\r\n"
+ " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB (rounded so that it accepts 1.9MB but not 2.1MB) Input:PDF Output:ZIP-PDF Type:SISO")
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request)
throws Exception {
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<ByteArrayOutputStream>();
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
PDDocument sourceDocument = PDDocument.load(file.getInputStream()); PDDocument sourceDocument = PDDocument.load(file.getInputStream());
//0 = size, 1 = page count, 2 = doc count // 0 = size, 1 = page count, 2 = doc count
int type = request.getSplitType(); int type = request.getSplitType();
String value = request.getSplitValue(); String value = request.getSplitValue();
if (type == 0) { // Split by size if (type == 0) { // Split by size
long maxBytes = GeneralUtils.convertSizeToBytes(value); long maxBytes = GeneralUtils.convertSizeToBytes(value);
long currentSize = 0; long currentSize = 0;
@@ -93,7 +96,7 @@ public class SplitPdfBySizeController {
splitDocumentsBoas.add(currentDocToByteArray(currentDoc)); splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
} }
} else if (type == 2) { // Split by doc count } else if (type == 2) { // Split by doc count
int documentCount = Integer.parseInt(value); int documentCount = Integer.parseInt(value);
int totalPageCount = sourceDocument.getNumberOfPages(); int totalPageCount = sourceDocument.getNumberOfPages();
int pagesPerDocument = totalPageCount / documentCount; int pagesPerDocument = totalPageCount / documentCount;
int extraPages = totalPageCount % documentCount; int extraPages = totalPageCount % documentCount;
@@ -114,9 +117,7 @@ public class SplitPdfBySizeController {
} }
sourceDocument.close(); sourceDocument.close();
Path zipFile = Files.createTempFile("split_documents", ".zip"); Path zipFile = Files.createTempFile("split_documents", ".zip");
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", ""); String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
byte[] data; byte[] data;
@@ -135,19 +136,18 @@ public class SplitPdfBySizeController {
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
data = Files.readAllBytes(zipFile); data = Files.readAllBytes(zipFile);
Files.delete(zipFile); Files.delete(zipFile);
} }
return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} }
private ByteArrayOutputStream currentDocToByteArray(PDDocument document) throws IOException { private ByteArrayOutputStream currentDocToByteArray(PDDocument document) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos); document.save(baos);
document.close(); document.close();
return baos; return baos;
} }
} }

View File

@@ -20,8 +20,10 @@ import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
@@ -29,58 +31,61 @@ public class ToSinglePageController {
private static final Logger logger = LoggerFactory.getLogger(ToSinglePageController.class); private static final Logger logger = LoggerFactory.getLogger(ToSinglePageController.class);
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-single-page") @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-single-page")
@Operation( @Operation(
summary = "Convert a multi-page PDF into a single long page PDF", summary = "Convert a multi-page PDF into a single long page PDF",
description = "This endpoint converts a multi-page PDF document into a single paged PDF document. The width of the single page will be same as the input's width, but the height will be the sum of all the pages' heights. Input:PDF Output:PDF Type:SISO" description =
) "This endpoint converts a multi-page PDF document into a single paged PDF document. The width of the single page will be same as the input's width, but the height will be the sum of all the pages' heights. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> pdfToSinglePage(@ModelAttribute PDFFile request) throws IOException { public ResponseEntity<byte[]> pdfToSinglePage(@ModelAttribute PDFFile request)
throws IOException {
// Load the source document // Load the source document
PDDocument sourceDocument = PDDocument.load(request.getFileInput().getInputStream()); PDDocument sourceDocument = PDDocument.load(request.getFileInput().getInputStream());
// Calculate total height and max width // Calculate total height and max width
float totalHeight = 0; float totalHeight = 0;
float maxWidth = 0; float maxWidth = 0;
for (PDPage page : sourceDocument.getPages()) { for (PDPage page : sourceDocument.getPages()) {
PDRectangle pageSize = page.getMediaBox(); PDRectangle pageSize = page.getMediaBox();
totalHeight += pageSize.getHeight(); totalHeight += pageSize.getHeight();
maxWidth = Math.max(maxWidth, pageSize.getWidth()); maxWidth = Math.max(maxWidth, pageSize.getWidth());
} }
// Create new document and page with calculated dimensions // Create new document and page with calculated dimensions
PDDocument newDocument = new PDDocument(); PDDocument newDocument = new PDDocument();
PDPage newPage = new PDPage(new PDRectangle(maxWidth, totalHeight)); PDPage newPage = new PDPage(new PDRectangle(maxWidth, totalHeight));
newDocument.addPage(newPage); newDocument.addPage(newPage);
// Initialize the content stream of the new page // Initialize the content stream of the new page
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage); PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage);
contentStream.close(); contentStream.close();
LayerUtility layerUtility = new LayerUtility(newDocument);
float yOffset = totalHeight;
// For each page, copy its content to the new page at the correct offset LayerUtility layerUtility = new LayerUtility(newDocument);
for (PDPage page : sourceDocument.getPages()) { float yOffset = totalHeight;
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, sourceDocument.getPages().indexOf(page));
AffineTransform af = AffineTransform.getTranslateInstance(0, yOffset - page.getMediaBox().getHeight());
layerUtility.wrapInSaveRestore(newPage);
String defaultLayerName = "Layer" + sourceDocument.getPages().indexOf(page);
layerUtility.appendFormAsLayer(newPage, form, af, defaultLayerName);
yOffset -= page.getMediaBox().getHeight();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream(); // For each page, copy its content to the new page at the correct offset
newDocument.save(baos); for (PDPage page : sourceDocument.getPages()) {
newDocument.close(); PDFormXObject form =
sourceDocument.close(); layerUtility.importPageAsForm(
sourceDocument, sourceDocument.getPages().indexOf(page));
AffineTransform af =
AffineTransform.getTranslateInstance(
0, yOffset - page.getMediaBox().getHeight());
layerUtility.wrapInSaveRestore(newPage);
String defaultLayerName = "Layer" + sourceDocument.getPages().indexOf(page);
layerUtility.appendFormAsLayer(newPage, form, af, defaultLayerName);
yOffset -= page.getMediaBox().getHeight();
}
byte[] result = baos.toByteArray(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
return WebResponseUtils.bytesToWebResponse(result, request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_singlePage.pdf"); newDocument.save(baos);
newDocument.close();
sourceDocument.close();
byte[] result = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(
result,
request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_singlePage.pdf");
} }
} }

View File

@@ -29,14 +29,14 @@ import stirling.software.SPDF.model.User;
@Controller @Controller
@RequestMapping("/api/v1/user") @RequestMapping("/api/v1/user")
public class UserController { public class UserController {
@Autowired @Autowired private UserService userService;
private UserService userService;
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/register") @PostMapping("/register")
public String register(@RequestParam String username, @RequestParam String password, Model model) { public String register(
if(userService.usernameExists(username)) { @RequestParam String username, @RequestParam String password, Model model) {
if (userService.usernameExists(username)) {
model.addAttribute("error", "Username already exists"); model.addAttribute("error", "Username already exists");
return "register"; return "register";
} }
@@ -44,39 +44,41 @@ public class UserController {
userService.saveUser(username, password); userService.saveUser(username, password);
return "redirect:/login?registered=true"; return "redirect:/login?registered=true";
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/change-username-and-password") @PostMapping("/change-username-and-password")
public RedirectView changeUsernameAndPassword(Principal principal, public RedirectView changeUsernameAndPassword(
@RequestParam String currentPassword, Principal principal,
@RequestParam String newUsername, @RequestParam String currentPassword,
@RequestParam String newPassword, @RequestParam String newUsername,
HttpServletRequest request, @RequestParam String newPassword,
HttpServletResponse response, HttpServletRequest request,
RedirectAttributes redirectAttributes) { HttpServletResponse response,
if (principal == null) { RedirectAttributes redirectAttributes) {
return new RedirectView("/change-creds?messageType=notAuthenticated"); if (principal == null) {
} return new RedirectView("/change-creds?messageType=notAuthenticated");
}
Optional<User> userOpt = userService.findByUsername(principal.getName()); Optional<User> userOpt = userService.findByUsername(principal.getName());
if (userOpt == null || userOpt.isEmpty()) { if (userOpt == null || userOpt.isEmpty()) {
return new RedirectView("/change-creds?messageType=userNotFound"); return new RedirectView("/change-creds?messageType=userNotFound");
} }
User user = userOpt.get(); User user = userOpt.get();
if (!userService.isPasswordCorrect(user, currentPassword)) { if (!userService.isPasswordCorrect(user, currentPassword)) {
return new RedirectView("/change-creds?messageType=incorrectPassword"); return new RedirectView("/change-creds?messageType=incorrectPassword");
} }
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
return new RedirectView("/change-creds?messageType=usernameExists");
}
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
return new RedirectView("/change-creds?messageType=usernameExists");
}
userService.changePassword(user, newPassword); userService.changePassword(user, newPassword);
if(newUsername != null && newUsername.length() > 0 && !user.getUsername().equals(newUsername)) { if (newUsername != null
&& newUsername.length() > 0
&& !user.getUsername().equals(newUsername)) {
userService.changeUsername(user, newUsername); userService.changeUsername(user, newUsername);
} }
userService.changeFirstUse(user, false); userService.changeFirstUse(user, false);
@@ -87,36 +89,36 @@ public class UserController {
return new RedirectView("/login?messageType=credsUpdated"); return new RedirectView("/login?messageType=credsUpdated");
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/change-username") @PostMapping("/change-username")
public RedirectView changeUsername(Principal principal, public RedirectView changeUsername(
@RequestParam String currentPassword, Principal principal,
@RequestParam String newUsername, @RequestParam String currentPassword,
HttpServletRequest request, @RequestParam String newUsername,
HttpServletResponse response, HttpServletRequest request,
RedirectAttributes redirectAttributes) { HttpServletResponse response,
if (principal == null) { RedirectAttributes redirectAttributes) {
return new RedirectView("/account?messageType=notAuthenticated"); if (principal == null) {
} return new RedirectView("/account?messageType=notAuthenticated");
}
Optional<User> userOpt = userService.findByUsername(principal.getName()); Optional<User> userOpt = userService.findByUsername(principal.getName());
if (userOpt == null || userOpt.isEmpty()) { if (userOpt == null || userOpt.isEmpty()) {
return new RedirectView("/account?messageType=userNotFound"); return new RedirectView("/account?messageType=userNotFound");
} }
User user = userOpt.get(); User user = userOpt.get();
if (!userService.isPasswordCorrect(user, currentPassword)) { if (!userService.isPasswordCorrect(user, currentPassword)) {
return new RedirectView("/account?messageType=incorrectPassword"); return new RedirectView("/account?messageType=incorrectPassword");
} }
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) { if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
return new RedirectView("/account?messageType=usernameExists"); return new RedirectView("/account?messageType=usernameExists");
} }
if(newUsername != null && newUsername.length() > 0) { if (newUsername != null && newUsername.length() > 0) {
userService.changeUsername(user, newUsername); userService.changeUsername(user, newUsername);
} }
@@ -125,30 +127,31 @@ public class UserController {
return new RedirectView("/login?messageType=credsUpdated"); return new RedirectView("/login?messageType=credsUpdated");
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/change-password") @PostMapping("/change-password")
public RedirectView changePassword(Principal principal, public RedirectView changePassword(
@RequestParam String currentPassword, Principal principal,
@RequestParam String newPassword, @RequestParam String currentPassword,
HttpServletRequest request, @RequestParam String newPassword,
HttpServletResponse response, HttpServletRequest request,
RedirectAttributes redirectAttributes) { HttpServletResponse response,
if (principal == null) { RedirectAttributes redirectAttributes) {
return new RedirectView("/account?messageType=notAuthenticated"); if (principal == null) {
} return new RedirectView("/account?messageType=notAuthenticated");
}
Optional<User> userOpt = userService.findByUsername(principal.getName()); Optional<User> userOpt = userService.findByUsername(principal.getName());
if (userOpt == null || userOpt.isEmpty()) { if (userOpt == null || userOpt.isEmpty()) {
return new RedirectView("/account?messageType=userNotFound"); return new RedirectView("/account?messageType=userNotFound");
} }
User user = userOpt.get(); User user = userOpt.get();
if (!userService.isPasswordCorrect(user, currentPassword)) { if (!userService.isPasswordCorrect(user, currentPassword)) {
return new RedirectView("/account?messageType=incorrectPassword"); return new RedirectView("/account?messageType=incorrectPassword");
} }
userService.changePassword(user, newPassword); userService.changePassword(user, newPassword);
@@ -160,33 +163,37 @@ public class UserController {
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/updateUserSettings") @PostMapping("/updateUserSettings")
public String updateUserSettings(HttpServletRequest request, Principal principal) { public String updateUserSettings(HttpServletRequest request, Principal principal) {
Map<String, String[]> paramMap = request.getParameterMap(); Map<String, String[]> paramMap = request.getParameterMap();
Map<String, String> updates = new HashMap<>(); Map<String, String> updates = new HashMap<>();
System.out.println("Received parameter map: " + paramMap); System.out.println("Received parameter map: " + paramMap);
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) { for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
updates.put(entry.getKey(), entry.getValue()[0]); updates.put(entry.getKey(), entry.getValue()[0]);
} }
System.out.println("Processed updates: " + updates); System.out.println("Processed updates: " + updates);
// Assuming you have a method in userService to update the settings for a user // Assuming you have a method in userService to update the settings for a user
userService.updateUserSettings(principal.getName(), updates); userService.updateUserSettings(principal.getName(), updates);
return "redirect:/account"; // Redirect to a page of your choice after updating return "redirect:/account"; // Redirect to a page of your choice after updating
} }
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/saveUser") @PostMapping("/admin/saveUser")
public RedirectView saveUser(@RequestParam String username, @RequestParam String password, @RequestParam String role, public RedirectView saveUser(
@RequestParam(name = "forceChange", required = false, defaultValue = "false") boolean forceChange) { @RequestParam String username,
@RequestParam String password,
if(userService.usernameExists(username)) { @RequestParam String role,
return new RedirectView("/addUsers?messageType=usernameExists"); @RequestParam(name = "forceChange", required = false, defaultValue = "false")
} boolean forceChange) {
try {
if (userService.usernameExists(username)) {
return new RedirectView("/addUsers?messageType=usernameExists");
}
try {
// Validate the role // Validate the role
Role roleEnum = Role.fromString(role); Role roleEnum = Role.fromString(role);
if (roleEnum == Role.INTERNAL_API_USER) { if (roleEnum == Role.INTERNAL_API_USER) {
@@ -197,28 +204,27 @@ public class UserController {
// If the role ID is not valid, redirect with an error message // If the role ID is not valid, redirect with an error message
return new RedirectView("/addUsers?messageType=invalidRole"); return new RedirectView("/addUsers?messageType=invalidRole");
} }
userService.saveUser(username, password, role, forceChange); userService.saveUser(username, password, role, forceChange);
return new RedirectView("/addUsers"); // Redirect to account page after adding the user return new RedirectView("/addUsers"); // Redirect to account page after adding the user
} }
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/deleteUser/{username}") @PostMapping("/admin/deleteUser/{username}")
public String deleteUser(@PathVariable String username, Authentication authentication) { public String deleteUser(@PathVariable String username, Authentication authentication) {
// Get the currently authenticated username // Get the currently authenticated username
String currentUsername = authentication.getName(); String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username // Check if the provided username matches the current session's username
if (currentUsername.equals(username)) { if (currentUsername.equals(username)) {
throw new IllegalArgumentException("Cannot delete currently logined in user."); throw new IllegalArgumentException("Cannot delete currently logined in user.");
} }
userService.deleteUser(username); userService.deleteUser(username);
return "redirect:/addUsers"; return "redirect:/addUsers";
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/get-api-key") @PostMapping("/get-api-key")
public ResponseEntity<String> getApiKey(Principal principal) { public ResponseEntity<String> getApiKey(Principal principal) {
@@ -247,6 +253,4 @@ public class UserController {
} }
return ResponseEntity.ok(apiKey); return ResponseEntity.ok(apiKey);
} }
} }

View File

@@ -0,0 +1,68 @@
package stirling.software.SPDF.controller.api.converters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.GeneralFile;
import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
@RequestMapping("/api/v1/convert")
public class ConvertBookToPDFController {
@Autowired
@Qualifier("bookFormatsInstalled")
private boolean bookFormatsInstalled;
@PostMapping(consumes = "multipart/form-data", value = "/book/pdf")
@Operation(
summary =
"Convert a BOOK/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx) to PDF",
description =
"(Requires bookFormatsInstalled flag and Calibre installed) This endpoint takes an BOOK/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx) input and converts it to PDF format.")
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute GeneralFile request) throws Exception {
MultipartFile fileInput = request.getFileInput();
if (!bookFormatsInstalled) {
throw new IllegalArgumentException(
"bookFormatsInstalled flag is False, this functionality is not avaiable");
}
if (fileInput == null) {
throw new IllegalArgumentException("Please provide a file for conversion.");
}
String originalFilename = fileInput.getOriginalFilename();
if (originalFilename != null) {
String originalFilenameLower = originalFilename.toLowerCase();
if (!originalFilenameLower.endsWith(".epub")
&& !originalFilenameLower.endsWith(".mobi")
&& !originalFilenameLower.endsWith(".azw3")
&& !originalFilenameLower.endsWith(".fb2")
&& !originalFilenameLower.endsWith(".txt")
&& !originalFilenameLower.endsWith(".docx")) {
throw new IllegalArgumentException(
"File must be in .epub, .mobi, .azw3, .fb2, .txt, or .docx format.");
}
}
byte[] pdfBytes = FileToPdf.convertBookTypeToPdf(fileInput.getBytes(), originalFilename);
String outputFilename =
originalFilename.replaceFirst("[.][^.]+$", "")
+ ".pdf"; // Remove file extension and append .pdf
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
}
}

View File

@@ -1,5 +1,7 @@
package stirling.software.SPDF.controller.api.converters; package stirling.software.SPDF.controller.api.converters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -9,6 +11,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.GeneralFile; import stirling.software.SPDF.model.api.GeneralFile;
import stirling.software.SPDF.utils.FileToPdf; import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@@ -18,35 +21,36 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RequestMapping("/api/v1/convert") @RequestMapping("/api/v1/convert")
public class ConvertHtmlToPDF { public class ConvertHtmlToPDF {
@Autowired
@Qualifier("htmlFormatsInstalled")
private boolean htmlFormatsInstalled;
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf") @PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
@Operation( @Operation(
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF", summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
description = "This endpoint takes an HTML or ZIP file input and converts it to a PDF format." description =
) "This endpoint takes an HTML or ZIP file input and converts it to a PDF format.")
public ResponseEntity<byte[]> HtmlToPdf( public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute GeneralFile request) throws Exception {
@ModelAttribute GeneralFile request) MultipartFile fileInput = request.getFileInput();
throws Exception {
MultipartFile fileInput = request.getFileInput();
if (fileInput == null) { if (fileInput == null) {
throw new IllegalArgumentException("Please provide an HTML or ZIP file for conversion."); throw new IllegalArgumentException(
} "Please provide an HTML or ZIP file for conversion.");
}
String originalFilename = fileInput.getOriginalFilename(); String originalFilename = fileInput.getOriginalFilename();
if (originalFilename == null || (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) { if (originalFilename == null
throw new IllegalArgumentException("File must be either .html or .zip format."); || (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) {
}byte[] pdfBytes = FileToPdf.convertHtmlToPdf( fileInput.getBytes(), originalFilename); throw new IllegalArgumentException("File must be either .html or .zip format.");
}
String outputFilename = originalFilename.replaceFirst("[.][^.]+$", "") + ".pdf"; // Remove file extension and append .pdf byte[] pdfBytes =
FileToPdf.convertHtmlToPdf(
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); fileInput.getBytes(), originalFilename, htmlFormatsInstalled);
}
String outputFilename =
originalFilename.replaceFirst("[.][^.]+$", "")
+ ".pdf"; // Remove file extension and append .pdf
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
}
} }

View File

@@ -20,6 +20,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest; import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest; import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
@@ -33,15 +34,18 @@ public class ConvertImgPDFController {
private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class); private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class);
@PostMapping(consumes = "multipart/form-data", value = "/pdf/img") @PostMapping(consumes = "multipart/form-data", value = "/pdf/img")
@Operation(summary = "Convert PDF to image(s)", @Operation(
description = "This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images. Input:PDF Output:Image Type:SI-Conditional") summary = "Convert PDF to image(s)",
public ResponseEntity<Resource> convertToImage(@ModelAttribute ConvertToImageRequest request) throws IOException { description =
"This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images. Input:PDF Output:Image Type:SI-Conditional")
public ResponseEntity<Resource> convertToImage(@ModelAttribute ConvertToImageRequest request)
throws IOException {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
String imageFormat = request.getImageFormat(); String imageFormat = request.getImageFormat();
String singleOrMultiple = request.getSingleOrMultiple(); String singleOrMultiple = request.getSingleOrMultiple();
String colorType = request.getColorType(); String colorType = request.getColorType();
String dpi = request.getDpi(); String dpi = request.getDpi();
byte[] pdfBytes = file.getBytes(); byte[] pdfBytes = file.getBytes();
ImageType colorTypeResult = ImageType.RGB; ImageType colorTypeResult = ImageType.RGB;
if ("greyscale".equals(colorType)) { if ("greyscale".equals(colorType)) {
@@ -54,7 +58,14 @@ public class ConvertImgPDFController {
byte[] result = null; byte[] result = null;
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", ""); String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
try { try {
result = PdfUtils.convertFromPdf(pdfBytes, imageFormat.toUpperCase(), colorTypeResult, singleImage, Integer.valueOf(dpi), filename); result =
PdfUtils.convertFromPdf(
pdfBytes,
imageFormat.toUpperCase(),
colorTypeResult,
singleImage,
Integer.valueOf(dpi),
filename);
} catch (IOException e) { } catch (IOException e) {
// TODO Auto-generated catch block // TODO Auto-generated catch block
e.printStackTrace(); e.printStackTrace();
@@ -65,29 +76,39 @@ public class ConvertImgPDFController {
if (singleImage) { if (singleImage) {
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType(getMediaType(imageFormat))); headers.setContentType(MediaType.parseMediaType(getMediaType(imageFormat)));
ResponseEntity<Resource> response = new ResponseEntity<>(new ByteArrayResource(result), headers, HttpStatus.OK); ResponseEntity<Resource> response =
new ResponseEntity<>(new ByteArrayResource(result), headers, HttpStatus.OK);
return response; return response;
} else { } else {
ByteArrayResource resource = new ByteArrayResource(result); ByteArrayResource resource = new ByteArrayResource(result);
// return the Resource in the response // return the Resource in the response
return ResponseEntity.ok() return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename + "_convertedToImages.zip") .header(
.contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(resource.contentLength()).body(resource); HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=" + filename + "_convertedToImages.zip")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(resource.contentLength())
.body(resource);
} }
} }
@PostMapping(consumes = "multipart/form-data", value = "/img/pdf") @PostMapping(consumes = "multipart/form-data", value = "/img/pdf")
@Operation(summary = "Convert images to a PDF file", @Operation(
description = "This endpoint converts one or more images to a PDF file. Users can specify whether to stretch the images to fit the PDF page, and whether to automatically rotate the images. Input:Image Output:PDF Type:SISO?") summary = "Convert images to a PDF file",
public ResponseEntity<byte[]> convertToPdf(@ModelAttribute ConvertToPdfRequest request) throws IOException { description =
"This endpoint converts one or more images to a PDF file. Users can specify whether to stretch the images to fit the PDF page, and whether to automatically rotate the images. Input:Image Output:PDF Type:SISO?")
public ResponseEntity<byte[]> convertToPdf(@ModelAttribute ConvertToPdfRequest request)
throws IOException {
MultipartFile[] file = request.getFileInput(); MultipartFile[] file = request.getFileInput();
String fitOption = request.getFitOption(); String fitOption = request.getFitOption();
String colorType = request.getColorType(); String colorType = request.getColorType();
boolean autoRotate = request.isAutoRotate(); boolean autoRotate = request.isAutoRotate();
// Convert the file to PDF and get the resulting bytes // Convert the file to PDF and get the resulting bytes
byte[] bytes = PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType); byte[] bytes = PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType);
return WebResponseUtils.bytesToWebResponse(bytes, file[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_converted.pdf"); return WebResponseUtils.bytesToWebResponse(
bytes,
file[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_converted.pdf");
} }
private String getMediaType(String imageFormat) { private String getMediaType(String imageFormat) {

View File

@@ -3,6 +3,8 @@ package stirling.software.SPDF.controller.api.converters;
import org.commonmark.node.Node; import org.commonmark.node.Node;
import org.commonmark.parser.Parser; import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer; import org.commonmark.renderer.html.HtmlRenderer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -12,6 +14,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.GeneralFile; import stirling.software.SPDF.model.api.GeneralFile;
import stirling.software.SPDF.utils.FileToPdf; import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@@ -20,17 +23,20 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Convert", description = "Convert APIs") @Tag(name = "Convert", description = "Convert APIs")
@RequestMapping("/api/v1/convert") @RequestMapping("/api/v1/convert")
public class ConvertMarkdownToPdf { public class ConvertMarkdownToPdf {
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf") @Autowired
@Qualifier("htmlFormatsInstalled")
private boolean htmlFormatsInstalled;
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
@Operation( @Operation(
summary = "Convert a Markdown file to PDF", summary = "Convert a Markdown file to PDF",
description = "This endpoint takes a Markdown file input, converts it to HTML, and then to PDF format." description =
) "This endpoint takes a Markdown file input, converts it to HTML, and then to PDF format.")
public ResponseEntity<byte[]> markdownToPdf( public ResponseEntity<byte[]> markdownToPdf(@ModelAttribute GeneralFile request)
@ModelAttribute GeneralFile request) throws Exception {
throws Exception { MultipartFile fileInput = request.getFileInput();
MultipartFile fileInput = request.getFileInput();
if (fileInput == null) { if (fileInput == null) {
throw new IllegalArgumentException("Please provide a Markdown file for conversion."); throw new IllegalArgumentException("Please provide a Markdown file for conversion.");
} }
@@ -45,10 +51,14 @@ public class ConvertMarkdownToPdf {
Node document = parser.parse(new String(fileInput.getBytes())); Node document = parser.parse(new String(fileInput.getBytes()));
HtmlRenderer renderer = HtmlRenderer.builder().build(); HtmlRenderer renderer = HtmlRenderer.builder().build();
String htmlContent = renderer.render(document); String htmlContent = renderer.render(document);
byte[] pdfBytes = FileToPdf.convertHtmlToPdf(htmlContent.getBytes(), "converted.html");
String outputFilename = originalFilename.replaceFirst("[.][^.]+$", "") + ".pdf"; // Remove file extension and append .pdf byte[] pdfBytes =
FileToPdf.convertHtmlToPdf(
htmlContent.getBytes(), "converted.html", htmlFormatsInstalled);
String outputFilename =
originalFilename.replaceFirst("[.][^.]+$", "")
+ ".pdf"; // Remove file extension and append .pdf
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
} }
} }

View File

@@ -18,6 +18,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.GeneralFile; import stirling.software.SPDF.model.api.GeneralFile;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@@ -31,20 +32,33 @@ public class ConvertOfficeController {
public byte[] convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException { public byte[] convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
// Check for valid file extension // Check for valid file extension
String originalFilename = inputFile.getOriginalFilename(); String originalFilename = inputFile.getOriginalFilename();
if (originalFilename == null || !isValidFileExtension(FilenameUtils.getExtension(originalFilename))) { if (originalFilename == null
|| !isValidFileExtension(FilenameUtils.getExtension(originalFilename))) {
throw new IllegalArgumentException("Invalid file extension"); throw new IllegalArgumentException("Invalid file extension");
} }
// Save the uploaded file to a temporary location // Save the uploaded file to a temporary location
Path tempInputFile = Files.createTempFile("input_", "." + FilenameUtils.getExtension(originalFilename)); Path tempInputFile =
Files.createTempFile("input_", "." + FilenameUtils.getExtension(originalFilename));
Files.copy(inputFile.getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING); Files.copy(inputFile.getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING);
// Prepare the output file path // Prepare the output file path
Path tempOutputFile = Files.createTempFile("output_", ".pdf"); Path tempOutputFile = Files.createTempFile("output_", ".pdf");
// Run the LibreOffice command // Run the LibreOffice command
List<String> command = new ArrayList<>(Arrays.asList("unoconv", "-vvv", "-f", "pdf", "-o", tempOutputFile.toString(), tempInputFile.toString())); List<String> command =
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE).runCommandWithOutputHandling(command); new ArrayList<>(
Arrays.asList(
"unoconv",
"-vvv",
"-f",
"pdf",
"-o",
tempOutputFile.toString(),
tempInputFile.toString()));
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
.runCommandWithOutputHandling(command);
// Read the converted PDF file // Read the converted PDF file
byte[] pdfBytes = Files.readAllBytes(tempOutputFile); byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
@@ -55,6 +69,7 @@ public class ConvertOfficeController {
return pdfBytes; return pdfBytes;
} }
private boolean isValidFileExtension(String fileExtension) { private boolean isValidFileExtension(String fileExtension) {
String extensionPattern = "^(?i)[a-z0-9]{2,4}$"; String extensionPattern = "^(?i)[a-z0-9]{2,4}$";
return fileExtension.matches(extensionPattern); return fileExtension.matches(extensionPattern);
@@ -62,17 +77,19 @@ public class ConvertOfficeController {
@PostMapping(consumes = "multipart/form-data", value = "/file/pdf") @PostMapping(consumes = "multipart/form-data", value = "/file/pdf")
@Operation( @Operation(
summary = "Convert a file to a PDF using LibreOffice", summary = "Convert a file to a PDF using LibreOffice",
description = "This endpoint converts a given file to a PDF using LibreOffice API Input:Any Output:PDF Type:SISO" description =
) "This endpoint converts a given file to a PDF using LibreOffice API Input:ANY Output:PDF Type:SISO")
public ResponseEntity<byte[]> processFileToPDF(@ModelAttribute GeneralFile request) public ResponseEntity<byte[]> processFileToPDF(@ModelAttribute GeneralFile request)
throws Exception { throws Exception {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
// unused but can start server instance if startup time is to long // unused but can start server instance if startup time is to long
// LibreOfficeListener.getInstance().start(); // LibreOfficeListener.getInstance().start();
byte[] pdfByteArray = convertToPdf(inputFile); byte[] pdfByteArray = convertToPdf(inputFile);
return WebResponseUtils.bytesToWebResponse(pdfByteArray, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_convertedToPDF.pdf"); return WebResponseUtils.bytesToWebResponse(
pdfByteArray,
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_convertedToPDF.pdf");
} }
} }

View File

@@ -0,0 +1,101 @@
package stirling.software.SPDF.controller.api.converters;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.converters.PdfToBookRequest;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
@RequestMapping("/api/v1/convert")
public class ConvertPDFToBookController {
@Autowired
@Qualifier("bookFormatsInstalled")
private boolean bookFormatsInstalled;
@PostMapping(consumes = "multipart/form-data", value = "/pdf/book")
@Operation(
summary =
"Convert a PDF to a Book/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx .. (others to include by chatgpt) to PDF",
description =
"(Requires bookFormatsInstalled flag and Calibre installed) This endpoint Convert a PDF to a Book/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx .. (others to include by chatgpt) to PDF")
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute PdfToBookRequest request)
throws Exception {
MultipartFile fileInput = request.getFileInput();
if (!bookFormatsInstalled) {
throw new IllegalArgumentException(
"bookFormatsInstalled flag is False, this functionality is not avaiable");
}
if (fileInput == null) {
throw new IllegalArgumentException("Please provide a file for conversion.");
}
// Validate the output format
String outputFormat = request.getOutputFormat().toLowerCase();
List<String> allowedFormats =
Arrays.asList(
"epub", "mobi", "azw3", "docx", "rtf", "txt", "html", "lit", "fb2", "pdb",
"lrf");
if (!allowedFormats.contains(outputFormat)) {
throw new IllegalArgumentException("Invalid output format: " + outputFormat);
}
byte[] outputFileBytes;
List<String> command = new ArrayList<>();
Path tempOutputFile =
Files.createTempFile(
"output_",
"." + outputFormat); // Use the output format for the file extension
Path tempInputFile = null;
try {
// Create temp input file from the provided PDF
tempInputFile = Files.createTempFile("input_", ".pdf"); // Assuming input is always PDF
Files.write(tempInputFile, fileInput.getBytes());
command.add("ebook-convert");
command.add(tempInputFile.toString());
command.add(tempOutputFile.toString());
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.CALIBRE)
.runCommandWithOutputHandling(command);
outputFileBytes = Files.readAllBytes(tempOutputFile);
} finally {
// Clean up temporary files
if (tempInputFile != null) {
Files.deleteIfExists(tempInputFile);
}
Files.deleteIfExists(tempOutputFile);
}
String outputFilename =
fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "."
+ outputFormat; // Remove file extension and append .pdf
return WebResponseUtils.bytesToWebResponse(outputFileBytes, outputFilename);
}
}

View File

@@ -11,6 +11,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest; import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest;
import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest; import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest;
@@ -22,51 +23,70 @@ import stirling.software.SPDF.utils.PDFToFile;
@Tag(name = "Convert", description = "Convert APIs") @Tag(name = "Convert", description = "Convert APIs")
public class ConvertPDFToOffice { public class ConvertPDFToOffice {
@PostMapping(consumes = "multipart/form-data", value = "/pdf/html") @PostMapping(consumes = "multipart/form-data", value = "/pdf/html")
@Operation(summary = "Convert PDF to HTML", description = "This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO") @Operation(
public ResponseEntity<byte[]> processPdfToHTML(@ModelAttribute PDFFile request) summary = "Convert PDF to HTML",
throws Exception { description =
MultipartFile inputFile = request.getFileInput(); "This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO")
PDFToFile pdfToFile = new PDFToFile(); public ResponseEntity<byte[]> processPdfToHTML(@ModelAttribute PDFFile request)
return pdfToFile.processPdfToOfficeFormat(inputFile, "html", "writer_pdf_import"); throws Exception {
} MultipartFile inputFile = request.getFileInput();
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, "html", "writer_pdf_import");
}
@PostMapping(consumes = "multipart/form-data", value = "/pdf/presentation") @PostMapping(consumes = "multipart/form-data", value = "/pdf/presentation")
@Operation(summary = "Convert PDF to Presentation format", description = "This endpoint converts a given PDF file to a Presentation format. Input:PDF Output:PPT Type:SISO") @Operation(
public ResponseEntity<byte[]> processPdfToPresentation(@ModelAttribute PdfToPresentationRequest request) throws IOException, InterruptedException { summary = "Convert PDF to Presentation format",
MultipartFile inputFile = request.getFileInput(); description =
String outputFormat = request.getOutputFormat(); "This endpoint converts a given PDF file to a Presentation format. Input:PDF Output:PPT Type:SISO")
PDFToFile pdfToFile = new PDFToFile(); public ResponseEntity<byte[]> processPdfToPresentation(
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import"); @ModelAttribute PdfToPresentationRequest request)
} throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String outputFormat = request.getOutputFormat();
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import");
}
@PostMapping(consumes = "multipart/form-data", value = "/pdf/text") @PostMapping(consumes = "multipart/form-data", value = "/pdf/text")
@Operation(summary = "Convert PDF to Text or RTF format", description = "This endpoint converts a given PDF file to Text or RTF format. Input:PDF Output:TXT Type:SISO") @Operation(
public ResponseEntity<byte[]> processPdfToRTForTXT(@ModelAttribute PdfToTextOrRTFRequest request) throws IOException, InterruptedException { summary = "Convert PDF to Text or RTF format",
MultipartFile inputFile = request.getFileInput(); description =
String outputFormat = request.getOutputFormat(); "This endpoint converts a given PDF file to Text or RTF format. Input:PDF Output:TXT Type:SISO")
public ResponseEntity<byte[]> processPdfToRTForTXT(
@ModelAttribute PdfToTextOrRTFRequest request)
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String outputFormat = request.getOutputFormat();
PDFToFile pdfToFile = new PDFToFile(); PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
} }
@PostMapping(consumes = "multipart/form-data", value = "/pdf/word") @PostMapping(consumes = "multipart/form-data", value = "/pdf/word")
@Operation(summary = "Convert PDF to Word document", description = "This endpoint converts a given PDF file to a Word document format. Input:PDF Output:WORD Type:SISO") @Operation(
public ResponseEntity<byte[]> processPdfToWord(@ModelAttribute PdfToWordRequest request) throws IOException, InterruptedException { summary = "Convert PDF to Word document",
MultipartFile inputFile = request.getFileInput(); description =
String outputFormat = request.getOutputFormat(); "This endpoint converts a given PDF file to a Word document format. Input:PDF Output:WORD Type:SISO")
PDFToFile pdfToFile = new PDFToFile(); public ResponseEntity<byte[]> processPdfToWord(@ModelAttribute PdfToWordRequest request)
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); throws IOException, InterruptedException {
} MultipartFile inputFile = request.getFileInput();
String outputFormat = request.getOutputFormat();
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
}
@PostMapping(consumes = "multipart/form-data", value = "/pdf/xml") @PostMapping(consumes = "multipart/form-data", value = "/pdf/xml")
@Operation(summary = "Convert PDF to XML", description = "This endpoint converts a PDF file to an XML file. Input:PDF Output:XML Type:SISO") @Operation(
public ResponseEntity<byte[]> processPdfToXML(@ModelAttribute PDFFile request) summary = "Convert PDF to XML",
throws Exception { description =
MultipartFile inputFile = request.getFileInput(); "This endpoint converts a PDF file to an XML file. Input:PDF Output:XML Type:SISO")
public ResponseEntity<byte[]> processPdfToXML(@ModelAttribute PDFFile request)
PDFToFile pdfToFile = new PDFToFile(); throws Exception {
return pdfToFile.processPdfToOfficeFormat(inputFile, "xml", "writer_pdf_import"); MultipartFile inputFile = request.getFileInput();
}
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, "xml", "writer_pdf_import");
}
} }

View File

@@ -14,6 +14,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@@ -24,14 +25,13 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Convert", description = "Convert APIs") @Tag(name = "Convert", description = "Convert APIs")
public class ConvertPDFToPDFA { public class ConvertPDFToPDFA {
@PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa") @PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa")
@Operation( @Operation(
summary = "Convert a PDF to a PDF/A", summary = "Convert a PDF to a PDF/A",
description = "This endpoint converts a PDF file to a PDF/A file. PDF/A is a format designed for long-term archiving of digital documents. Input:PDF Output:PDF Type:SISO" description =
) "This endpoint converts a PDF file to a PDF/A file. PDF/A is a format designed for long-term archiving of digital documents. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> pdfToPdfA(@ModelAttribute PDFFile request) public ResponseEntity<byte[]> pdfToPdfA(@ModelAttribute PDFFile request) throws Exception {
throws Exception { MultipartFile inputFile = request.getFileInput();
MultipartFile inputFile = request.getFileInput();
// Save the uploaded file to a temporary location // Save the uploaded file to a temporary location
Path tempInputFile = Files.createTempFile("input_", ".pdf"); Path tempInputFile = Files.createTempFile("input_", ".pdf");
@@ -50,7 +50,9 @@ public class ConvertPDFToPDFA {
command.add(tempInputFile.toString()); command.add(tempInputFile.toString());
command.add(tempOutputFile.toString()); command.add(tempOutputFile.toString());
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command); ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
.runCommandWithOutputHandling(command);
// Read the optimized PDF file // Read the optimized PDF file
byte[] pdfBytes = Files.readAllBytes(tempOutputFile); byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
@@ -60,8 +62,8 @@ public class ConvertPDFToPDFA {
Files.delete(tempOutputFile); Files.delete(tempOutputFile);
// Return the optimized PDF as a response // Return the optimized PDF as a response
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_PDFA.pdf"; String outputFilename =
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_PDFA.pdf";
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
} }
} }

View File

@@ -1,76 +1,87 @@
package stirling.software.SPDF.controller.api.converters; package stirling.software.SPDF.controller.api.converters;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.springframework.http.ResponseEntity; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import io.swagger.v3.oas.annotations.Operation; import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest; import io.swagger.v3.oas.annotations.Operation;
import stirling.software.SPDF.utils.GeneralUtils; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
@RestController import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@Tag(name = "Convert", description = "Convert APIs") import stirling.software.SPDF.utils.WebResponseUtils;
@RequestMapping("/api/v1/convert")
public class ConvertWebsiteToPDF { @RestController
@Tag(name = "Convert", description = "Convert APIs")
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf") @RequestMapping("/api/v1/convert")
@Operation( public class ConvertWebsiteToPDF {
summary = "Convert a URL to a PDF",
description = "This endpoint fetches content from a URL and converts it to a PDF format. Input:N/A Output:PDF Type:SISO" @Autowired
) @Qualifier("htmlFormatsInstalled")
public ResponseEntity<byte[]> urlToPdf(@ModelAttribute UrlToPdfRequest request) throws IOException, InterruptedException { private boolean htmlFormatsInstalled;
String URL = request.getUrlInput();
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
// Validate the URL format @Operation(
if(!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) { summary = "Convert a URL to a PDF",
throw new IllegalArgumentException("Invalid URL format provided."); description =
} "This endpoint fetches content from a URL and converts it to a PDF format. Input:N/A Output:PDF Type:SISO")
Path tempOutputFile = null; public ResponseEntity<byte[]> urlToPdf(@ModelAttribute UrlToPdfRequest request)
byte[] pdfBytes; throws IOException, InterruptedException {
try { String URL = request.getUrlInput();
// Prepare the output file path
tempOutputFile = Files.createTempFile("output_", ".pdf"); // Validate the URL format
if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
// Prepare the OCRmyPDF command throw new IllegalArgumentException("Invalid URL format provided.");
List<String> command = new ArrayList<>(); }
command.add("weasyprint"); Path tempOutputFile = null;
command.add(URL); byte[] pdfBytes;
command.add(tempOutputFile.toString()); try {
// Prepare the output file path
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT).runCommandWithOutputHandling(command); tempOutputFile = Files.createTempFile("output_", ".pdf");
// Read the optimized PDF file // Prepare the OCRmyPDF command
pdfBytes = Files.readAllBytes(tempOutputFile); List<String> command = new ArrayList<>();
} if (!htmlFormatsInstalled) {
finally { command.add("weasyprint");
// Clean up the temporary files } else {
Files.delete(tempOutputFile); command.add("wkhtmltopdf");
} }
// Convert URL to a safe filename command.add(URL);
String outputFilename = convertURLToFileName(URL); command.add(tempOutputFile.toString());
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); ProcessExecutorResult returnCode =
} ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
.runCommandWithOutputHandling(command);
private String convertURLToFileName(String url) {
String safeName = url.replaceAll("[^a-zA-Z0-9]", "_"); // Read the optimized PDF file
if(safeName.length() > 50) { pdfBytes = Files.readAllBytes(tempOutputFile);
safeName = safeName.substring(0, 50); // restrict to 50 characters } finally {
} // Clean up the temporary files
return safeName + ".pdf"; Files.delete(tempOutputFile);
} }
// Convert URL to a safe filename
String outputFilename = convertURLToFileName(URL);
}
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
}
private String convertURLToFileName(String url) {
String safeName = url.replaceAll("[^a-zA-Z0-9]", "_");
if (safeName.length() > 50) {
safeName = safeName.substring(0, 50); // restrict to 50 characters
}
return safeName + ".pdf";
}
}

View File

@@ -22,6 +22,7 @@ import com.opencsv.CSVWriter;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.controller.api.CropController; import stirling.software.SPDF.controller.api.CropController;
import stirling.software.SPDF.controller.api.strippers.PDFTableStripper; import stirling.software.SPDF.controller.api.strippers.PDFTableStripper;
import stirling.software.SPDF.model.api.extract.PDFFilePage; import stirling.software.SPDF.model.api.extract.PDFFilePage;
@@ -34,21 +35,24 @@ public class ExtractController {
private static final Logger logger = LoggerFactory.getLogger(CropController.class); private static final Logger logger = LoggerFactory.getLogger(CropController.class);
@PostMapping(value = "/pdf/csv", consumes = "multipart/form-data") @PostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
@Operation(summary = "Extracts a PDF document to csv", description = "This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO") @Operation(
public ResponseEntity<String> PdfToCsv(@ModelAttribute PDFFilePage form) summary = "Extracts a PDF document to csv",
throws Exception { description =
"This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO")
public ResponseEntity<String> PdfToCsv(@ModelAttribute PDFFilePage form) throws Exception {
ArrayList<String> tableData = new ArrayList<>(); ArrayList<String> tableData = new ArrayList<>();
int columnsCount = 0; int columnsCount = 0;
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) { try (PDDocument document =
PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) {
final double res = 72; // PDF units are at 72 DPI final double res = 72; // PDF units are at 72 DPI
PDFTableStripper stripper = new PDFTableStripper(); PDFTableStripper stripper = new PDFTableStripper();
PDPage pdPage = document.getPage(form.getPageId() - 1); PDPage pdPage = document.getPage(form.getPageId() - 1);
stripper.extractTable(pdPage); stripper.extractTable(pdPage);
columnsCount = stripper.getColumns(); columnsCount = stripper.getColumns();
for (int c = 0; c < columnsCount; ++c) { for (int c = 0; c < columnsCount; ++c) {
for(int r=0; r<stripper.getRows(); ++r) { for (int r = 0; r < stripper.getRows(); ++r) {
tableData.add(stripper.getText(r, c)); tableData.add(stripper.getText(r, c));
} }
} }
@@ -56,26 +60,33 @@ public class ExtractController {
ArrayList<String> notEmptyColumns = new ArrayList<>(); ArrayList<String> notEmptyColumns = new ArrayList<>();
for (String item: tableData) { for (String item : tableData) {
if(!item.trim().isEmpty()){ if (!item.trim().isEmpty()) {
notEmptyColumns.add(item); notEmptyColumns.add(item);
}else{ } else {
columnsCount--; columnsCount--;
} }
} }
List<String> fullTable = notEmptyColumns.stream().map((entity)-> List<String> fullTable =
entity.replace('\n',' ').replace('\r',' ').trim().replaceAll("\\s{2,}", "|")).toList(); notEmptyColumns.stream()
.map(
(entity) ->
entity.replace('\n', ' ')
.replace('\r', ' ')
.trim()
.replaceAll("\\s{2,}", "|"))
.toList();
int rowsCount = fullTable.get(0).split("\\|").length; int rowsCount = fullTable.get(0).split("\\|").length;
ArrayList<String> headersList = getTableHeaders(columnsCount,fullTable); ArrayList<String> headersList = getTableHeaders(columnsCount, fullTable);
ArrayList<String> recordList = getRecordsList(rowsCount,fullTable); ArrayList<String> recordList = getRecordsList(rowsCount, fullTable);
if(headersList.size() == 0 && recordList.size() == 0) { if (headersList.size() == 0 && recordList.size() == 0) {
throw new Exception("No table detected, no headers or records found"); throw new Exception("No table detected, no headers or records found");
} }
StringWriter writer = new StringWriter(); StringWriter writer = new StringWriter();
try (CSVWriter csvWriter = new CSVWriter(writer)) { try (CSVWriter csvWriter = new CSVWriter(writer)) {
csvWriter.writeNext(headersList.toArray(new String[0])); csvWriter.writeNext(headersList.toArray(new String[0]));
@@ -85,35 +96,41 @@ public class ExtractController {
} }
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setContentDisposition(ContentDisposition.builder("attachment").filename(form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_extracted.csv").build()); headers.setContentDisposition(
ContentDisposition.builder("attachment")
.filename(
form.getFileInput()
.getOriginalFilename()
.replaceFirst("[.][^.]+$", "")
+ "_extracted.csv")
.build());
headers.setContentType(MediaType.parseMediaType("text/csv")); headers.setContentType(MediaType.parseMediaType("text/csv"));
return ResponseEntity.ok() return ResponseEntity.ok().headers(headers).body(writer.toString());
.headers(headers)
.body(writer.toString());
} }
private ArrayList<String> getRecordsList( int rowsCounts ,List<String> items){ private ArrayList<String> getRecordsList(int rowsCounts, List<String> items) {
ArrayList<String> recordsList = new ArrayList<>(); ArrayList<String> recordsList = new ArrayList<>();
for (int b=1; b<rowsCounts;b++) { for (int b = 1; b < rowsCounts; b++) {
StringBuilder strbldr = new StringBuilder(); StringBuilder strbldr = new StringBuilder();
for (int i=0;i<items.size();i++){ for (int i = 0; i < items.size(); i++) {
String[] parts = items.get(i).split("\\|"); String[] parts = items.get(i).split("\\|");
strbldr.append(parts[b]); strbldr.append(parts[b]);
if (i!= items.size()-1){ if (i != items.size() - 1) {
strbldr.append("|"); strbldr.append("|");
}
} }
recordsList.add(strbldr.toString());
} }
recordsList.add(strbldr.toString());
}
return recordsList; return recordsList;
} }
private ArrayList<String> getTableHeaders(int columnsCount, List<String> items){
private ArrayList<String> getTableHeaders(int columnsCount, List<String> items) {
ArrayList<String> resultList = new ArrayList<>(); ArrayList<String> resultList = new ArrayList<>();
for (int i=0;i<columnsCount;i++){ for (int i = 0; i < columnsCount; i++) {
String[] parts = items.get(i).split("\\|"); String[] parts = items.get(i).split("\\|");
resultList.add(parts[0]); resultList.add(parts[0]);
} }

View File

@@ -1,196 +1,210 @@
package stirling.software.SPDF.controller.api.filters; package stirling.software.SPDF.controller.api.filters;
import java.io.IOException; import java.io.IOException;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFComparisonAndCount;
import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.model.api.PDFComparisonAndCount;
import stirling.software.SPDF.model.api.filter.ContainsTextRequest; import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.model.api.filter.FileSizeRequest; import stirling.software.SPDF.model.api.filter.ContainsTextRequest;
import stirling.software.SPDF.model.api.filter.PageRotationRequest; import stirling.software.SPDF.model.api.filter.FileSizeRequest;
import stirling.software.SPDF.model.api.filter.PageSizeRequest; import stirling.software.SPDF.model.api.filter.PageRotationRequest;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.model.api.filter.PageSizeRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/filter") @RestController
@Tag(name = "Filter", description = "Filter APIs") @RequestMapping("/api/v1/filter")
public class FilterController { @Tag(name = "Filter", description = "Filter APIs")
public class FilterController {
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text")
@Operation(summary = "Checks if a PDF contains set text, returns true if does", description = "Input:PDF Output:Boolean Type:SISO") @PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text")
public ResponseEntity<byte[]> containsText(@ModelAttribute ContainsTextRequest request) throws IOException, InterruptedException { @Operation(
MultipartFile inputFile = request.getFileInput(); summary = "Checks if a PDF contains set text, returns true if does",
String text = request.getText(); description = "Input:PDF Output:Boolean Type:SISO")
String pageNumber = request.getPageNumbers(); public ResponseEntity<byte[]> containsText(@ModelAttribute ContainsTextRequest request)
throws IOException, InterruptedException {
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); MultipartFile inputFile = request.getFileInput();
if (PdfUtils.hasText(pdfDocument, pageNumber, text)) String text = request.getText();
return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename()); String pageNumber = request.getPageNumbers();
return null;
} PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
if (PdfUtils.hasText(pdfDocument, pageNumber, text))
// TODO return WebResponseUtils.pdfDocToWebResponse(
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image") pdfDocument, inputFile.getOriginalFilename());
@Operation(summary = "Checks if a PDF contains an image", description = "Input:PDF Output:Boolean Type:SISO") return null;
public ResponseEntity<byte[]> containsImage(@ModelAttribute PDFWithPageNums request) }
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput(); // TODO
String pageNumber = request.getPageNumbers(); @PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image")
@Operation(
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); summary = "Checks if a PDF contains an image",
if (PdfUtils.hasImages(pdfDocument, pageNumber)) description = "Input:PDF Output:Boolean Type:SISO")
return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename()); public ResponseEntity<byte[]> containsImage(@ModelAttribute PDFWithPageNums request)
return null; throws IOException, InterruptedException {
} MultipartFile inputFile = request.getFileInput();
String pageNumber = request.getPageNumbers();
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-count")
@Operation(summary = "Checks if a PDF is greater, less or equal to a setPageCount", description = "Input:PDF Output:Boolean Type:SISO") PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
public ResponseEntity<byte[]> pageCount(@ModelAttribute PDFComparisonAndCount request) throws IOException, InterruptedException { if (PdfUtils.hasImages(pdfDocument, pageNumber))
MultipartFile inputFile = request.getFileInput(); return WebResponseUtils.pdfDocToWebResponse(
String pageCount = request.getPageCount(); pdfDocument, inputFile.getOriginalFilename());
String comparator = request.getComparator(); return null;
// Load the PDF }
PDDocument document = PDDocument.load(inputFile.getInputStream());
int actualPageCount = document.getNumberOfPages(); @PostMapping(consumes = "multipart/form-data", value = "/filter-page-count")
@Operation(
boolean valid = false; summary = "Checks if a PDF is greater, less or equal to a setPageCount",
// Perform the comparison description = "Input:PDF Output:Boolean Type:SISO")
switch (comparator) { public ResponseEntity<byte[]> pageCount(@ModelAttribute PDFComparisonAndCount request)
case "Greater": throws IOException, InterruptedException {
valid = actualPageCount > Integer.parseInt(pageCount); MultipartFile inputFile = request.getFileInput();
break; String pageCount = request.getPageCount();
case "Equal": String comparator = request.getComparator();
valid = actualPageCount == Integer.parseInt(pageCount); // Load the PDF
break; PDDocument document = PDDocument.load(inputFile.getInputStream());
case "Less": int actualPageCount = document.getNumberOfPages();
valid = actualPageCount < Integer.parseInt(pageCount);
break; boolean valid = false;
default: // Perform the comparison
throw new IllegalArgumentException("Invalid comparator: " + comparator); switch (comparator) {
} case "Greater":
valid = actualPageCount > Integer.parseInt(pageCount);
if (valid) break;
return WebResponseUtils.multiPartFileToWebResponse(inputFile); case "Equal":
return null; valid = actualPageCount == Integer.parseInt(pageCount);
} break;
case "Less":
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-size") valid = actualPageCount < Integer.parseInt(pageCount);
@Operation(summary = "Checks if a PDF is of a certain size", description = "Input:PDF Output:Boolean Type:SISO") break;
public ResponseEntity<byte[]> pageSize(@ModelAttribute PageSizeRequest request) throws IOException, InterruptedException { default:
MultipartFile inputFile = request.getFileInput(); throw new IllegalArgumentException("Invalid comparator: " + comparator);
String standardPageSize = request.getStandardPageSize(); }
String comparator = request.getComparator();
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
// Load the PDF return null;
PDDocument document = PDDocument.load(inputFile.getInputStream()); }
PDPage firstPage = document.getPage(0); @PostMapping(consumes = "multipart/form-data", value = "/filter-page-size")
PDRectangle actualPageSize = firstPage.getMediaBox(); @Operation(
summary = "Checks if a PDF is of a certain size",
// Calculate the area of the actual page size description = "Input:PDF Output:Boolean Type:SISO")
float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight(); public ResponseEntity<byte[]> pageSize(@ModelAttribute PageSizeRequest request)
throws IOException, InterruptedException {
// Get the standard size and calculate its area MultipartFile inputFile = request.getFileInput();
PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize); String standardPageSize = request.getStandardPageSize();
float standardArea = standardSize.getWidth() * standardSize.getHeight(); String comparator = request.getComparator();
boolean valid = false; // Load the PDF
// Perform the comparison PDDocument document = PDDocument.load(inputFile.getInputStream());
switch (comparator) {
case "Greater": PDPage firstPage = document.getPage(0);
valid = actualArea > standardArea; PDRectangle actualPageSize = firstPage.getMediaBox();
break;
case "Equal": // Calculate the area of the actual page size
valid = actualArea == standardArea; float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight();
break;
case "Less": // Get the standard size and calculate its area
valid = actualArea < standardArea; PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize);
break; float standardArea = standardSize.getWidth() * standardSize.getHeight();
default:
throw new IllegalArgumentException("Invalid comparator: " + comparator); boolean valid = false;
} // Perform the comparison
switch (comparator) {
if (valid) case "Greater":
return WebResponseUtils.multiPartFileToWebResponse(inputFile); valid = actualArea > standardArea;
return null; break;
} case "Equal":
valid = actualArea == standardArea;
@PostMapping(consumes = "multipart/form-data", value = "/filter-file-size") break;
@Operation(summary = "Checks if a PDF is a set file size", description = "Input:PDF Output:Boolean Type:SISO") case "Less":
public ResponseEntity<byte[]> fileSize(@ModelAttribute FileSizeRequest request) throws IOException, InterruptedException { valid = actualArea < standardArea;
MultipartFile inputFile = request.getFileInput(); break;
String fileSize = request.getFileSize(); default:
String comparator = request.getComparator(); throw new IllegalArgumentException("Invalid comparator: " + comparator);
}
// Get the file size
long actualFileSize = inputFile.getSize(); if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
return null;
boolean valid = false; }
// Perform the comparison
switch (comparator) { @PostMapping(consumes = "multipart/form-data", value = "/filter-file-size")
case "Greater": @Operation(
valid = actualFileSize > Long.parseLong(fileSize); summary = "Checks if a PDF is a set file size",
break; description = "Input:PDF Output:Boolean Type:SISO")
case "Equal": public ResponseEntity<byte[]> fileSize(@ModelAttribute FileSizeRequest request)
valid = actualFileSize == Long.parseLong(fileSize); throws IOException, InterruptedException {
break; MultipartFile inputFile = request.getFileInput();
case "Less": String fileSize = request.getFileSize();
valid = actualFileSize < Long.parseLong(fileSize); String comparator = request.getComparator();
break;
default: // Get the file size
throw new IllegalArgumentException("Invalid comparator: " + comparator); long actualFileSize = inputFile.getSize();
}
boolean valid = false;
if (valid) // Perform the comparison
return WebResponseUtils.multiPartFileToWebResponse(inputFile); switch (comparator) {
return null; case "Greater":
} valid = actualFileSize > Long.parseLong(fileSize);
break;
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation") case "Equal":
@Operation(summary = "Checks if a PDF is of a certain rotation", description = "Input:PDF Output:Boolean Type:SISO") valid = actualFileSize == Long.parseLong(fileSize);
public ResponseEntity<byte[]> pageRotation(@ModelAttribute PageRotationRequest request) throws IOException, InterruptedException { break;
MultipartFile inputFile = request.getFileInput(); case "Less":
int rotation = request.getRotation(); valid = actualFileSize < Long.parseLong(fileSize);
String comparator = request.getComparator(); break;
default:
// Load the PDF throw new IllegalArgumentException("Invalid comparator: " + comparator);
PDDocument document = PDDocument.load(inputFile.getInputStream()); }
// Get the rotation of the first page if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
PDPage firstPage = document.getPage(0); return null;
int actualRotation = firstPage.getRotation(); }
boolean valid = false;
// Perform the comparison @PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation")
switch (comparator) { @Operation(
case "Greater": summary = "Checks if a PDF is of a certain rotation",
valid = actualRotation > rotation; description = "Input:PDF Output:Boolean Type:SISO")
break; public ResponseEntity<byte[]> pageRotation(@ModelAttribute PageRotationRequest request)
case "Equal": throws IOException, InterruptedException {
valid = actualRotation == rotation; MultipartFile inputFile = request.getFileInput();
break; int rotation = request.getRotation();
case "Less": String comparator = request.getComparator();
valid = actualRotation < rotation;
break; // Load the PDF
default: PDDocument document = PDDocument.load(inputFile.getInputStream());
throw new IllegalArgumentException("Invalid comparator: " + comparator);
} // Get the rotation of the first page
PDPage firstPage = document.getPage(0);
if (valid) int actualRotation = firstPage.getRotation();
return WebResponseUtils.multiPartFileToWebResponse(inputFile); boolean valid = false;
return null; // Perform the comparison
switch (comparator) {
} case "Greater":
valid = actualRotation > rotation;
} break;
case "Equal":
valid = actualRotation == rotation;
break;
case "Less":
valid = actualRotation < rotation;
break;
default:
throw new IllegalArgumentException("Invalid comparator: " + comparator);
}
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
return null;
}
}

View File

@@ -19,8 +19,10 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest; import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
@@ -32,97 +34,105 @@ public class AutoRenameController {
private static final int LINE_LIMIT = 11; private static final int LINE_LIMIT = 11;
@PostMapping(consumes = "multipart/form-data", value = "/auto-rename") @PostMapping(consumes = "multipart/form-data", value = "/auto-rename")
@Operation(summary = "Extract header from PDF file", description = "This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO") @Operation(
public ResponseEntity<byte[]> extractHeader(@ModelAttribute ExtractHeaderRequest request) throws Exception { summary = "Extract header from PDF file",
description =
"This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> extractHeader(@ModelAttribute ExtractHeaderRequest request)
throws Exception {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
Boolean useFirstTextAsFallback = request.isUseFirstTextAsFallback(); Boolean useFirstTextAsFallback = request.isUseFirstTextAsFallback();
PDDocument document = PDDocument.load(file.getInputStream()); PDDocument document = PDDocument.load(file.getInputStream());
PDFTextStripper reader = new PDFTextStripper() { PDFTextStripper reader =
class LineInfo { new PDFTextStripper() {
String text; class LineInfo {
float fontSize; String text;
float fontSize;
LineInfo(String text, float fontSize) { LineInfo(String text, float fontSize) {
this.text = text; this.text = text;
this.fontSize = fontSize; this.fontSize = fontSize;
} }
} }
List<LineInfo> lineInfos = new ArrayList<>(); List<LineInfo> lineInfos = new ArrayList<>();
StringBuilder lineBuilder = new StringBuilder(); StringBuilder lineBuilder = new StringBuilder();
float lastY = -1; float lastY = -1;
float maxFontSizeInLine = 0.0f; float maxFontSizeInLine = 0.0f;
int lineCount = 0; int lineCount = 0;
@Override @Override
protected void processTextPosition(TextPosition text) { protected void processTextPosition(TextPosition text) {
if (lastY != text.getY() && lineCount < LINE_LIMIT) { if (lastY != text.getY() && lineCount < LINE_LIMIT) {
processLine(); processLine();
lineBuilder = new StringBuilder(text.getUnicode()); lineBuilder = new StringBuilder(text.getUnicode());
maxFontSizeInLine = text.getFontSizeInPt(); maxFontSizeInLine = text.getFontSizeInPt();
lastY = text.getY(); lastY = text.getY();
lineCount++; lineCount++;
} else if (lineCount < LINE_LIMIT) { } else if (lineCount < LINE_LIMIT) {
lineBuilder.append(text.getUnicode()); lineBuilder.append(text.getUnicode());
if (text.getFontSizeInPt() > maxFontSizeInLine) { if (text.getFontSizeInPt() > maxFontSizeInLine) {
maxFontSizeInLine = text.getFontSizeInPt(); maxFontSizeInLine = text.getFontSizeInPt();
} }
} }
} }
private void processLine() { private void processLine() {
if (lineBuilder.length() > 0 && lineCount < LINE_LIMIT) { if (lineBuilder.length() > 0 && lineCount < LINE_LIMIT) {
lineInfos.add(new LineInfo(lineBuilder.toString(), maxFontSizeInLine)); lineInfos.add(new LineInfo(lineBuilder.toString(), maxFontSizeInLine));
} }
} }
@Override @Override
public String getText(PDDocument doc) throws IOException { public String getText(PDDocument doc) throws IOException {
this.lineInfos.clear(); this.lineInfos.clear();
this.lineBuilder = new StringBuilder(); this.lineBuilder = new StringBuilder();
this.lastY = -1; this.lastY = -1;
this.maxFontSizeInLine = 0.0f; this.maxFontSizeInLine = 0.0f;
this.lineCount = 0; this.lineCount = 0;
super.getText(doc); super.getText(doc);
processLine(); // Process the last line processLine(); // Process the last line
// Merge lines with same font size // Merge lines with same font size
List<LineInfo> mergedLineInfos = new ArrayList<>(); List<LineInfo> mergedLineInfos = new ArrayList<>();
for (int i = 0; i < lineInfos.size(); i++) { for (int i = 0; i < lineInfos.size(); i++) {
String mergedText = lineInfos.get(i).text; String mergedText = lineInfos.get(i).text;
float fontSize = lineInfos.get(i).fontSize; float fontSize = lineInfos.get(i).fontSize;
while (i + 1 < lineInfos.size() && lineInfos.get(i + 1).fontSize == fontSize) { while (i + 1 < lineInfos.size()
mergedText += " " + lineInfos.get(i + 1).text; && lineInfos.get(i + 1).fontSize == fontSize) {
i++; mergedText += " " + lineInfos.get(i + 1).text;
} i++;
mergedLineInfos.add(new LineInfo(mergedText, fontSize)); }
} mergedLineInfos.add(new LineInfo(mergedText, fontSize));
}
// Sort lines by font size in descending order and get the first one // Sort lines by font size in descending order and get the first one
mergedLineInfos.sort(Comparator.comparing((LineInfo li) -> li.fontSize).reversed()); mergedLineInfos.sort(
String title = mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(0).text; Comparator.comparing((LineInfo li) -> li.fontSize).reversed());
String title =
mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(0).text;
return title != null ? title : (useFirstTextAsFallback ? (mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(mergedLineInfos.size() - 1).text) : null); return title != null
} ? title
: (useFirstTextAsFallback
? (mergedLineInfos.isEmpty()
? null
: mergedLineInfos.get(mergedLineInfos.size() - 1)
.text)
: null);
}
};
}; String header = reader.getText(document);
String header = reader.getText(document);
// Sanitize the header string by removing characters not allowed in a filename. // Sanitize the header string by removing characters not allowed in a filename.
if (header != null && header.length() < 255) { if (header != null && header.length() < 255) {
header = header.replaceAll("[/\\\\?%*:|\"<>]", ""); header = header.replaceAll("[/\\\\?%*:|\"<>]", "");
return WebResponseUtils.pdfDocToWebResponse(document, header + ".pdf"); return WebResponseUtils.pdfDocToWebResponse(document, header + ".pdf");
} else { } else {
logger.info("File has no good title to be found"); logger.info("File has no good title to be found");
return WebResponseUtils.pdfDocToWebResponse(document, file.getOriginalFilename()); return WebResponseUtils.pdfDocToWebResponse(document, file.getOriginalFilename());
} }
} }
} }

View File

@@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte; import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt; import java.awt.image.DataBufferInt;
@@ -32,6 +33,7 @@ import com.google.zxing.common.HybridBinarizer;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest; import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@@ -40,11 +42,15 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class AutoSplitPdfController { public class AutoSplitPdfController {
private static final String QR_CONTENT = "https://github.com/Frooodle/Stirling-PDF"; private static final String QR_CONTENT = "https://github.com/Stirling-Tools/Stirling-PDF";
@PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data") @PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data")
@Operation(summary = "Auto split PDF pages into separate documents", description = "This endpoint accepts a PDF file, scans each page for a specific QR code, and splits the document at the QR code boundaries. The output is a zip file containing each separate PDF document. Input:PDF Output:ZIP-PDF Type:SISO") @Operation(
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request) throws IOException { summary = "Auto split PDF pages into separate documents",
description =
"This endpoint accepts a PDF file, scans each page for a specific QR code, and splits the document at the QR code boundaries. The output is a zip file containing each separate PDF document. Input:PDF Output:ZIP-PDF Type:SISO")
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request)
throws IOException {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
boolean duplexMode = request.isDuplexMode(); boolean duplexMode = request.isDuplexMode();
@@ -107,29 +113,48 @@ public class AutoSplitPdfController {
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
data = Files.readAllBytes(zipFile); data = Files.readAllBytes(zipFile);
Files.delete(zipFile); Files.delete(zipFile);
} }
return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} }
private static String decodeQRCode(BufferedImage bufferedImage) { private static String decodeQRCode(BufferedImage bufferedImage) {
LuminanceSource source; LuminanceSource source;
if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte) { if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte) {
byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData(); byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData();
source = new PlanarYUVLuminanceSource(pixels, bufferedImage.getWidth(), bufferedImage.getHeight(), 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight(), false); source =
new PlanarYUVLuminanceSource(
pixels,
bufferedImage.getWidth(),
bufferedImage.getHeight(),
0,
0,
bufferedImage.getWidth(),
bufferedImage.getHeight(),
false);
} else if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferInt) { } else if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferInt) {
int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData(); int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
byte[] newPixels = new byte[pixels.length]; byte[] newPixels = new byte[pixels.length];
for (int i = 0; i < pixels.length; i++) { for (int i = 0; i < pixels.length; i++) {
newPixels[i] = (byte) (pixels[i] & 0xff); newPixels[i] = (byte) (pixels[i] & 0xff);
} }
source = new PlanarYUVLuminanceSource(newPixels, bufferedImage.getWidth(), bufferedImage.getHeight(), 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight(), false); source =
new PlanarYUVLuminanceSource(
newPixels,
bufferedImage.getWidth(),
bufferedImage.getHeight(),
0,
0,
bufferedImage.getWidth(),
bufferedImage.getHeight(),
false);
} else { } else {
throw new IllegalArgumentException("BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed int), byte gray, or 3-byte/4-byte RGB image data"); throw new IllegalArgumentException(
"BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed int), byte gray, or 3-byte/4-byte RGB image data");
} }
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));

View File

@@ -28,6 +28,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest; import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
@@ -39,17 +40,18 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class BlankPageController { public class BlankPageController {
@PostMapping(consumes = "multipart/form-data", value = "/remove-blanks") @PostMapping(consumes = "multipart/form-data", value = "/remove-blanks")
@Operation( @Operation(
summary = "Remove blank pages from a PDF file", summary = "Remove blank pages from a PDF file",
description = "This endpoint removes blank pages from a given PDF file. Users can specify the threshold and white percentage to tune the detection of blank pages. Input:PDF Output:PDF Type:SISO" description =
) "This endpoint removes blank pages from a given PDF file. Users can specify the threshold and white percentage to tune the detection of blank pages. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> removeBlankPages(@ModelAttribute RemoveBlankPagesRequest request) throws IOException, InterruptedException { public ResponseEntity<byte[]> removeBlankPages(@ModelAttribute RemoveBlankPagesRequest request)
MultipartFile inputFile = request.getFileInput(); throws IOException, InterruptedException {
int threshold = request.getThreshold(); MultipartFile inputFile = request.getFileInput();
float whitePercent = request.getWhitePercent(); int threshold = request.getThreshold();
float whitePercent = request.getWhitePercent();
PDDocument document = null;
PDDocument document = null;
try { try {
document = PDDocument.load(inputFile.getInputStream()); document = PDDocument.load(inputFile.getInputStream());
PDPageTree pages = document.getDocumentCatalog().getPages(); PDPageTree pages = document.getDocumentCatalog().getPages();
@@ -72,21 +74,34 @@ public class BlankPageController {
boolean hasImages = PdfUtils.hasImagesOnPage(page); boolean hasImages = PdfUtils.hasImagesOnPage(page);
if (hasImages) { if (hasImages) {
System.out.println("page " + pageIndex + " has image"); System.out.println("page " + pageIndex + " has image");
Path tempFile = Files.createTempFile("image_", ".png"); Path tempFile = Files.createTempFile("image_", ".png");
// Render image and save as temp file // Render image and save as temp file
BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 300); BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 300);
ImageIO.write(image, "png", tempFile.toFile()); ImageIO.write(image, "png", tempFile.toFile());
List<String> command = new ArrayList<>(Arrays.asList("python3", System.getProperty("user.dir") + "/scripts/detect-blank-pages.py", tempFile.toString() ,"--threshold", String.valueOf(threshold), "--white_percent", String.valueOf(whitePercent))); List<String> command =
new ArrayList<>(
Arrays.asList(
"python3",
System.getProperty("user.dir")
+ "/scripts/detect-blank-pages.py",
tempFile.toString(),
"--threshold",
String.valueOf(threshold),
"--white_percent",
String.valueOf(whitePercent)));
// Run CLI command // Run CLI command
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV).runCommandWithOutputHandling(command); ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
.runCommandWithOutputHandling(command);
// does contain data // does contain data
if (returnCode.getRc() == 0) { if (returnCode.getRc() == 0) {
System.out.println("page " + pageIndex + " has image which is not blank"); System.out.println(
"page " + pageIndex + " has image which is not blank");
pagesToKeepIndex.add(pageIndex); pagesToKeepIndex.add(pageIndex);
} else { } else {
System.out.println("Skipping, Image was blank for page #" + pageIndex); System.out.println("Skipping, Image was blank for page #" + pageIndex);
@@ -94,12 +109,12 @@ public class BlankPageController {
} }
} }
pageIndex++; pageIndex++;
} }
System.out.print("pagesToKeep=" + pagesToKeepIndex.size()); System.out.print("pagesToKeep=" + pagesToKeepIndex.size());
// Remove pages not present in pagesToKeepIndex // Remove pages not present in pagesToKeepIndex
List<Integer> pageIndices = IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList()); List<Integer> pageIndices =
IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList());
Collections.reverse(pageIndices); // Reverse to prevent index shifting during removal Collections.reverse(pageIndices); // Reverse to prevent index shifting during removal
for (Integer i : pageIndices) { for (Integer i : pageIndices) {
if (!pagesToKeepIndex.contains(i)) { if (!pagesToKeepIndex.contains(i)) {
@@ -107,16 +122,15 @@ public class BlankPageController {
} }
} }
return WebResponseUtils.pdfDocToWebResponse(document, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_blanksRemoved.pdf"); return WebResponseUtils.pdfDocToWebResponse(
document,
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_blanksRemoved.pdf");
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
} finally { } finally {
if (document != null) if (document != null) document.close();
document.close();
} }
} }
} }

View File

@@ -30,6 +30,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest; import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
@@ -44,20 +45,23 @@ public class CompressController {
private static final Logger logger = LoggerFactory.getLogger(CompressController.class); private static final Logger logger = LoggerFactory.getLogger(CompressController.class);
@PostMapping(consumes = "multipart/form-data", value = "/compress-pdf") @PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
@Operation(summary = "Optimize PDF file", description = "This endpoint accepts a PDF file and optimizes it based on the provided parameters. Input:PDF Output:PDF Type:SISO") @Operation(
public ResponseEntity<byte[]> optimizePdf(@ModelAttribute OptimizePdfRequest request) throws Exception { summary = "Optimize PDF file",
description =
"This endpoint accepts a PDF file and optimizes it based on the provided parameters. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> optimizePdf(@ModelAttribute OptimizePdfRequest request)
throws Exception {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
Integer optimizeLevel = request.getOptimizeLevel(); Integer optimizeLevel = request.getOptimizeLevel();
String expectedOutputSizeString = request.getExpectedOutputSize(); String expectedOutputSizeString = request.getExpectedOutputSize();
if (expectedOutputSizeString == null && optimizeLevel == null) {
if(expectedOutputSizeString == null && optimizeLevel == null) {
throw new Exception("Both expected output size and optimize level are not specified"); throw new Exception("Both expected output size and optimize level are not specified");
} }
Long expectedOutputSize = 0L; Long expectedOutputSize = 0L;
boolean autoMode = false; boolean autoMode = false;
if (expectedOutputSizeString != null && expectedOutputSizeString.length() > 1 ) { if (expectedOutputSizeString != null && expectedOutputSizeString.length() > 1) {
expectedOutputSize = GeneralUtils.convertSizeToBytes(expectedOutputSizeString); expectedOutputSize = GeneralUtils.convertSizeToBytes(expectedOutputSizeString);
autoMode = true; autoMode = true;
} }
@@ -71,8 +75,9 @@ public class CompressController {
// Prepare the output file path // Prepare the output file path
Path tempOutputFile = Files.createTempFile("output_", ".pdf"); Path tempOutputFile = Files.createTempFile("output_", ".pdf");
// Determine initial optimization level based on expected size reduction, only if in autoMode // Determine initial optimization level based on expected size reduction, only if in
if(autoMode) { // autoMode
if (autoMode) {
double sizeReductionRatio = expectedOutputSize / (double) inputFileSize; double sizeReductionRatio = expectedOutputSize / (double) inputFileSize;
if (sizeReductionRatio > 0.7) { if (sizeReductionRatio > 0.7) {
optimizeLevel = 1; optimizeLevel = 1;
@@ -94,20 +99,20 @@ public class CompressController {
command.add("-dCompatibilityLevel=1.4"); command.add("-dCompatibilityLevel=1.4");
switch (optimizeLevel) { switch (optimizeLevel) {
case 1: case 1:
command.add("-dPDFSETTINGS=/prepress"); command.add("-dPDFSETTINGS=/prepress");
break; break;
case 2: case 2:
command.add("-dPDFSETTINGS=/printer"); command.add("-dPDFSETTINGS=/printer");
break; break;
case 3: case 3:
command.add("-dPDFSETTINGS=/ebook"); command.add("-dPDFSETTINGS=/ebook");
break; break;
case 4: case 4:
command.add("-dPDFSETTINGS=/screen"); command.add("-dPDFSETTINGS=/screen");
break; break;
default: default:
command.add("-dPDFSETTINGS=/default"); command.add("-dPDFSETTINGS=/default");
} }
command.add("-dNOPAUSE"); command.add("-dNOPAUSE");
@@ -116,7 +121,9 @@ public class CompressController {
command.add("-sOutputFile=" + tempOutputFile.toString()); command.add("-sOutputFile=" + tempOutputFile.toString());
command.add(tempInputFile.toString()); command.add(tempInputFile.toString());
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(command); ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
.runCommandWithOutputHandling(command);
// Check if file size is within expected size or not auto mode so instantly finish // Check if file size is within expected size or not auto mode so instantly finish
long outputFileSize = Files.size(tempOutputFile); long outputFileSize = Files.size(tempOutputFile);
@@ -125,19 +132,18 @@ public class CompressController {
} else { } else {
// Increase optimization level for next iteration // Increase optimization level for next iteration
optimizeLevel++; optimizeLevel++;
if(autoMode && optimizeLevel > 3) { if (autoMode && optimizeLevel > 3) {
System.out.println("Skipping level 4 due to bad results in auto mode"); System.out.println("Skipping level 4 due to bad results in auto mode");
sizeMet = true; sizeMet = true;
} else if(optimizeLevel == 5) { } else if (optimizeLevel == 5) {
} else { } else {
System.out.println("Increasing ghostscript optimisation level to " + optimizeLevel); System.out.println(
"Increasing ghostscript optimisation level to " + optimizeLevel);
} }
} }
} }
if (expectedOutputSize != null && autoMode) { if (expectedOutputSize != null && autoMode) {
long outputFileSize = Files.size(tempOutputFile); long outputFileSize = Files.size(tempOutputFile);
if (outputFileSize > expectedOutputSize) { if (outputFileSize > expectedOutputSize) {
@@ -157,8 +163,8 @@ public class CompressController {
BufferedImage bufferedImage = image.getImage(); BufferedImage bufferedImage = image.getImage();
// Calculate the new dimensions // Calculate the new dimensions
int newWidth = (int)(bufferedImage.getWidth() * scaleFactor); int newWidth = (int) (bufferedImage.getWidth() * scaleFactor);
int newHeight = (int)(bufferedImage.getHeight() * scaleFactor); int newHeight = (int) (bufferedImage.getHeight() * scaleFactor);
// If the new dimensions are zero, skip this iteration // If the new dimensions are zero, skip this iteration
if (newWidth == 0 || newHeight == 0) { if (newWidth == 0 || newHeight == 0) {
@@ -166,23 +172,39 @@ public class CompressController {
} }
// Otherwise, proceed with the scaling // Otherwise, proceed with the scaling
Image scaledImage = bufferedImage.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH); Image scaledImage =
bufferedImage.getScaledInstance(
newWidth, newHeight, Image.SCALE_SMOOTH);
// Convert the scaled image back to a BufferedImage // Convert the scaled image back to a BufferedImage
BufferedImage scaledBufferedImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB); BufferedImage scaledBufferedImage =
scaledBufferedImage.getGraphics().drawImage(scaledImage, 0, 0, null); new BufferedImage(
newWidth,
newHeight,
BufferedImage.TYPE_INT_RGB);
scaledBufferedImage
.getGraphics()
.drawImage(scaledImage, 0, 0, null);
// Compress the scaled image // Compress the scaled image
ByteArrayOutputStream compressedImageStream = new ByteArrayOutputStream(); ByteArrayOutputStream compressedImageStream =
ImageIO.write(scaledBufferedImage, "jpeg", compressedImageStream); new ByteArrayOutputStream();
ImageIO.write(
scaledBufferedImage, "jpeg", compressedImageStream);
byte[] imageBytes = compressedImageStream.toByteArray(); byte[] imageBytes = compressedImageStream.toByteArray();
compressedImageStream.close(); compressedImageStream.close();
// Convert compressed image back to PDImageXObject // Convert compressed image back to PDImageXObject
ByteArrayInputStream bais = new ByteArrayInputStream(imageBytes); ByteArrayInputStream bais =
PDImageXObject compressedImage = PDImageXObject.createFromByteArray(doc, imageBytes, image.getCOSObject().toString()); new ByteArrayInputStream(imageBytes);
PDImageXObject compressedImage =
PDImageXObject.createFromByteArray(
doc,
imageBytes,
image.getCOSObject().toString());
// Replace the image in the resources with the compressed version // Replace the image in the resources with the compressed
// version
res.put(name, compressedImage); res.put(name, compressedImage);
} }
} }
@@ -194,16 +216,23 @@ public class CompressController {
long currentSize = Files.size(tempOutputFile); long currentSize = Files.size(tempOutputFile);
// Check if the overall PDF size is still larger than expectedOutputSize // Check if the overall PDF size is still larger than expectedOutputSize
if (currentSize > expectedOutputSize) { if (currentSize > expectedOutputSize) {
// Log the current file size and scaleFactor // Log the current file size and scaleFactor
System.out.println("Current file size: " + FileUtils.byteCountToDisplaySize(currentSize)); System.out.println(
"Current file size: "
+ FileUtils.byteCountToDisplaySize(currentSize));
System.out.println("Current scale factor: " + scaleFactor); System.out.println("Current scale factor: " + scaleFactor);
// The file is still too large, reduce scaleFactor and try again // The file is still too large, reduce scaleFactor and try again
scaleFactor *= 0.9; // reduce scaleFactor by 10% scaleFactor *= 0.9; // reduce scaleFactor by 10%
// Avoid scaleFactor being too small, causing the image to shrink to 0 // Avoid scaleFactor being too small, causing the image to shrink to 0
if(scaleFactor < 0.2 || previousFileSize == currentSize){ if (scaleFactor < 0.2 || previousFileSize == currentSize) {
throw new RuntimeException("Could not reach the desired size without excessively degrading image quality, lowest size recommended is " + FileUtils.byteCountToDisplaySize(currentSize) + ", " + currentSize + " bytes"); throw new RuntimeException(
"Could not reach the desired size without excessively degrading image quality, lowest size recommended is "
+ FileUtils.byteCountToDisplaySize(currentSize)
+ ", "
+ currentSize
+ " bytes");
} }
previousFileSize = currentSize; previousFileSize = currentSize;
} else { } else {
@@ -211,10 +240,7 @@ public class CompressController {
break; break;
} }
} }
} }
} }
} }
@@ -222,9 +248,10 @@ public class CompressController {
byte[] pdfBytes = Files.readAllBytes(tempOutputFile); byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
// Check if optimized file is larger than the original // Check if optimized file is larger than the original
if(pdfBytes.length > inputFileSize) { if (pdfBytes.length > inputFileSize) {
// Log the occurrence // Log the occurrence
logger.warn("Optimized file is larger than the original. Returning the original file instead."); logger.warn(
"Optimized file is larger than the original. Returning the original file instead.");
// Read the original file again // Read the original file again
pdfBytes = Files.readAllBytes(tempInputFile); pdfBytes = Files.readAllBytes(tempInputFile);
@@ -235,8 +262,8 @@ public class CompressController {
Files.delete(tempOutputFile); Files.delete(tempOutputFile);
// Return the optimized PDF as a response // Return the optimized PDF as a response
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_Optimized.pdf"; String outputFilename =
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_Optimized.pdf";
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
} }
} }

View File

@@ -32,10 +32,12 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest; import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
@@ -44,18 +46,28 @@ public class ExtractImageScansController {
private static final Logger logger = LoggerFactory.getLogger(ExtractImageScansController.class); private static final Logger logger = LoggerFactory.getLogger(ExtractImageScansController.class);
@PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans") @PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans")
@Operation(summary = "Extract image scans from an input file", @Operation(
description = "This endpoint extracts image scans from a given file based on certain parameters. Users can specify angle threshold, tolerance, minimum area, minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP Type:SIMO") summary = "Extract image scans from an input file",
description =
"This endpoint extracts image scans from a given file based on certain parameters. Users can specify angle threshold, tolerance, minimum area, minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP Type:SIMO")
public ResponseEntity<byte[]> extractImageScans( public ResponseEntity<byte[]> extractImageScans(
@RequestBody( @RequestBody(
description = "Form data containing file and extraction parameters", description = "Form data containing file and extraction parameters",
required = true, required = true,
content = @Content( content =
mediaType = "multipart/form-data", @Content(
schema = @Schema(implementation = ExtractImageScansRequest.class) // This should represent your form's structure mediaType = "multipart/form-data",
) schema =
) @Schema(
ExtractImageScansRequest form) throws IOException, InterruptedException { implementation =
ExtractImageScansRequest
.class) // This should
// represent
// your form's
// structure
))
ExtractImageScansRequest form)
throws IOException, InterruptedException {
String fileName = form.getFileInput().getOriginalFilename(); String fileName = form.getFileInput().getOriginalFilename();
String extension = fileName.substring(fileName.lastIndexOf(".") + 1); String extension = fileName.substring(fileName.lastIndexOf(".") + 1);
@@ -64,7 +76,8 @@ public class ExtractImageScansController {
// Check if input file is a PDF // Check if input file is a PDF
if (extension.equalsIgnoreCase("pdf")) { if (extension.equalsIgnoreCase("pdf")) {
// Load PDF document // Load PDF document
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) { try (PDDocument document =
PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) {
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);
int pageCount = document.getNumberOfPages(); int pageCount = document.getNumberOfPages();
images = new ArrayList<>(); images = new ArrayList<>();
@@ -84,7 +97,10 @@ public class ExtractImageScansController {
} }
} else { } else {
Path tempInputFile = Files.createTempFile("input_", "." + extension); Path tempInputFile = Files.createTempFile("input_", "." + extension);
Files.copy(form.getFileInput().getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING); Files.copy(
form.getFileInput().getInputStream(),
tempInputFile,
StandardCopyOption.REPLACE_EXISTING);
// Add input file path to images list // Add input file path to images list
images.add(tempInputFile.toString()); images.add(tempInputFile.toString());
} }
@@ -95,21 +111,28 @@ public class ExtractImageScansController {
for (int i = 0; i < images.size(); i++) { for (int i = 0; i < images.size(); i++) {
Path tempDir = Files.createTempDirectory("openCV_output"); Path tempDir = Files.createTempDirectory("openCV_output");
List<String> command = new ArrayList<>(Arrays.asList( List<String> command =
"python3", new ArrayList<>(
"./scripts/split_photos.py", Arrays.asList(
images.get(i), "python3",
tempDir.toString(), "./scripts/split_photos.py",
"--angle_threshold", String.valueOf(form.getAngleThreshold()), images.get(i),
"--tolerance", String.valueOf(form.getTolerance()), tempDir.toString(),
"--min_area", String.valueOf(form.getMinArea()), "--angle_threshold",
"--min_contour_area", String.valueOf(form.getMinContourArea()), String.valueOf(form.getAngleThreshold()),
"--border_size", String.valueOf(form.getBorderSize()) "--tolerance",
)); String.valueOf(form.getTolerance()),
"--min_area",
String.valueOf(form.getMinArea()),
"--min_contour_area",
String.valueOf(form.getMinContourArea()),
"--border_size",
String.valueOf(form.getBorderSize())));
// Run CLI command // Run CLI command
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV).runCommandWithOutputHandling(command); ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
.runCommandWithOutputHandling(command);
// Read the output photos in temp directory // Read the output photos in temp directory
List<Path> tempOutputFiles = Files.list(tempDir).sorted().collect(Collectors.toList()); List<Path> tempOutputFiles = Files.list(tempDir).sorted().collect(Collectors.toList());
@@ -126,10 +149,16 @@ public class ExtractImageScansController {
String outputZipFilename = fileName.replaceFirst("[.][^.]+$", "") + "_processed.zip"; String outputZipFilename = fileName.replaceFirst("[.][^.]+$", "") + "_processed.zip";
Path tempZipFile = Files.createTempFile("output_", ".zip"); Path tempZipFile = Files.createTempFile("output_", ".zip");
try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) { try (ZipOutputStream zipOut =
new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) {
// Add processed images to the zip // Add processed images to the zip
for (int i = 0; i < processedImageBytes.size(); i++) { for (int i = 0; i < processedImageBytes.size(); i++) {
ZipEntry entry = new ZipEntry(fileName.replaceFirst("[.][^.]+$", "") + "_" + (i + 1) + ".png"); ZipEntry entry =
new ZipEntry(
fileName.replaceFirst("[.][^.]+$", "")
+ "_"
+ (i + 1)
+ ".png");
zipOut.putNextEntry(entry); zipOut.putNextEntry(entry);
zipOut.write(processedImageBytes.get(i)); zipOut.write(processedImageBytes.get(i));
zipOut.closeEntry(); zipOut.closeEntry();
@@ -141,13 +170,15 @@ public class ExtractImageScansController {
// Clean up the temporary zip file // Clean up the temporary zip file
Files.delete(tempZipFile); Files.delete(tempZipFile);
return WebResponseUtils.bytesToWebResponse(zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.bytesToWebResponse(
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
} else { } else {
// Return the processed image as a response // Return the processed image as a response
byte[] imageBytes = processedImageBytes.get(0); byte[] imageBytes = processedImageBytes.get(0);
return WebResponseUtils.bytesToWebResponse(imageBytes, fileName.replaceFirst("[.][^.]+$", "") + ".png", MediaType.IMAGE_PNG); return WebResponseUtils.bytesToWebResponse(
imageBytes,
fileName.replaceFirst("[.][^.]+$", "") + ".png",
MediaType.IMAGE_PNG);
} }
} }
} }

View File

@@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.Image; import java.awt.Image;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
@@ -29,8 +30,10 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFWithImageFormatRequest; import stirling.software.SPDF.model.api.PDFWithImageFormatRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
@@ -39,13 +42,17 @@ public class ExtractImagesController {
private static final Logger logger = LoggerFactory.getLogger(ExtractImagesController.class); private static final Logger logger = LoggerFactory.getLogger(ExtractImagesController.class);
@PostMapping(consumes = "multipart/form-data", value = "/extract-images") @PostMapping(consumes = "multipart/form-data", value = "/extract-images")
@Operation(summary = "Extract images from a PDF file", @Operation(
description = "This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input:PDF Output:IMAGE/ZIP Type:SIMO") summary = "Extract images from a PDF file",
public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFWithImageFormatRequest request) throws IOException { description =
"This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input:PDF Output:IMAGE/ZIP Type:SIMO")
public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFWithImageFormatRequest request)
throws IOException {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
String format = request.getFormat(); String format = request.getFormat();
System.out.println(System.currentTimeMillis() + "file=" + file.getName() + ", format=" + format); System.out.println(
System.currentTimeMillis() + "file=" + file.getName() + ", format=" + format);
PDDocument document = PDDocument.load(file.getBytes()); PDDocument document = PDDocument.load(file.getBytes());
// Create ByteArrayOutputStream to write zip file to byte array // Create ByteArrayOutputStream to write zip file to byte array
@@ -69,24 +76,37 @@ public class ExtractImagesController {
if (page.getResources().isImageXObject(name)) { if (page.getResources().isImageXObject(name)) {
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name); PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
int imageHash = image.hashCode(); int imageHash = image.hashCode();
if(processedImages.contains(imageHash)) { if (processedImages.contains(imageHash)) {
continue; // Skip already processed images continue; // Skip already processed images
} }
processedImages.add(imageHash); processedImages.add(imageHash);
// Convert image to desired format // Convert image to desired format
RenderedImage renderedImage = image.getImage(); RenderedImage renderedImage = image.getImage();
BufferedImage bufferedImage = null; BufferedImage bufferedImage = null;
if (format.equalsIgnoreCase("png")) { if (format.equalsIgnoreCase("png")) {
bufferedImage = new BufferedImage(renderedImage.getWidth(), renderedImage.getHeight(), BufferedImage.TYPE_INT_ARGB); bufferedImage =
new BufferedImage(
renderedImage.getWidth(),
renderedImage.getHeight(),
BufferedImage.TYPE_INT_ARGB);
} else if (format.equalsIgnoreCase("jpeg") || format.equalsIgnoreCase("jpg")) { } else if (format.equalsIgnoreCase("jpeg") || format.equalsIgnoreCase("jpg")) {
bufferedImage = new BufferedImage(renderedImage.getWidth(), renderedImage.getHeight(), BufferedImage.TYPE_INT_RGB); bufferedImage =
new BufferedImage(
renderedImage.getWidth(),
renderedImage.getHeight(),
BufferedImage.TYPE_INT_RGB);
} else if (format.equalsIgnoreCase("gif")) { } else if (format.equalsIgnoreCase("gif")) {
bufferedImage = new BufferedImage(renderedImage.getWidth(), renderedImage.getHeight(), BufferedImage.TYPE_BYTE_INDEXED); bufferedImage =
new BufferedImage(
renderedImage.getWidth(),
renderedImage.getHeight(),
BufferedImage.TYPE_BYTE_INDEXED);
} }
// Write image to zip file // Write image to zip file
String imageName = filename + "_" + imageIndex + " (Page " + pageNum + ")." + format; String imageName =
filename + "_" + imageIndex + " (Page " + pageNum + ")." + format;
ZipEntry zipEntry = new ZipEntry(imageName); ZipEntry zipEntry = new ZipEntry(imageName);
zos.putNextEntry(zipEntry); zos.putNextEntry(zipEntry);
@@ -111,7 +131,7 @@ public class ExtractImagesController {
// Create ByteArrayResource from byte array // Create ByteArrayResource from byte array
byte[] zipContents = baos.toByteArray(); byte[] zipContents = baos.toByteArray();
return WebResponseUtils.boasToWebResponse(baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.boasToWebResponse(
baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM);
} }
} }

View File

@@ -3,21 +3,17 @@ package stirling.software.SPDF.controller.api.misc;
import java.awt.Color; import java.awt.Color;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp; import java.awt.image.AffineTransformOp;
//Required for image manipulation
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp; import java.awt.image.BufferedImageOp;
import java.awt.image.ConvolveOp; import java.awt.image.ConvolveOp;
import java.awt.image.Kernel; import java.awt.image.Kernel;
import java.awt.image.RescaleOp; import java.awt.image.RescaleOp;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
//Required for file input/output
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom; import java.security.SecureRandom;
//Other required classes
import java.util.Random; import java.util.Random;
//Required for image input/output
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
@@ -40,6 +36,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@@ -50,102 +47,101 @@ public class FakeScanControllerWIP {
private static final Logger logger = LoggerFactory.getLogger(FakeScanControllerWIP.class); private static final Logger logger = LoggerFactory.getLogger(FakeScanControllerWIP.class);
//TODO // TODO
@Hidden @Hidden
@PostMapping(consumes = "multipart/form-data", value = "/fakeScan") @PostMapping(consumes = "multipart/form-data", value = "/fakeScan")
@Operation( @Operation(
summary = "Repair a PDF file", summary = "Repair a PDF file",
description = "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response." description =
) "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response.")
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request) throws IOException { public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request) throws IOException {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
PDDocument document = PDDocument.load(inputFile.getBytes()); PDDocument document = PDDocument.load(inputFile.getBytes());
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);
for (int page = 0; page < document.getNumberOfPages(); ++page) for (int page = 0; page < document.getNumberOfPages(); ++page) {
{ BufferedImage image = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
BufferedImage image = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB); ImageIO.write(image, "png", new File("scanned-" + (page + 1) + ".png"));
ImageIO.write(image, "png", new File("scanned-" + (page+1) + ".png")); }
} document.close();
document.close();
// Constants // Constants
int scannedness = 90; // Value between 0 and 100 int scannedness = 90; // Value between 0 and 100
int dirtiness = 0; // Value between 0 and 100 int dirtiness = 0; // Value between 0 and 100
// Load the source image // Load the source image
BufferedImage sourceImage = ImageIO.read(new File("scanned-1.png")); BufferedImage sourceImage = ImageIO.read(new File("scanned-1.png"));
// Create the destination image // Create the destination image
BufferedImage destinationImage = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), sourceImage.getType()); BufferedImage destinationImage =
new BufferedImage(
sourceImage.getWidth(), sourceImage.getHeight(), sourceImage.getType());
// Apply a brightness and contrast effect based on the "scanned-ness" // Apply a brightness and contrast effect based on the "scanned-ness"
float scaleFactor = 1.0f + (scannedness / 100.0f) * 0.5f; // Between 1.0 and 1.5 float scaleFactor = 1.0f + (scannedness / 100.0f) * 0.5f; // Between 1.0 and 1.5
float offset = scannedness * 1.5f; // Between 0 and 150 float offset = scannedness * 1.5f; // Between 0 and 150
BufferedImageOp op = new RescaleOp(scaleFactor, offset, null); BufferedImageOp op = new RescaleOp(scaleFactor, offset, null);
op.filter(sourceImage, destinationImage); op.filter(sourceImage, destinationImage);
// Apply a rotation effect // Apply a rotation effect
double rotationRequired = Math.toRadians((new SecureRandom().nextInt(3 - 1) + 1)); // Random angle between 1 and 3 degrees double rotationRequired =
double locationX = destinationImage.getWidth() / 2; Math.toRadians(
double locationY = destinationImage.getHeight() / 2; (new SecureRandom().nextInt(3 - 1)
AffineTransform tx = AffineTransform.getRotateInstance(rotationRequired, locationX, locationY); + 1)); // Random angle between 1 and 3 degrees
AffineTransformOp rotateOp = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR); double locationX = destinationImage.getWidth() / 2;
destinationImage = rotateOp.filter(destinationImage, null); double locationY = destinationImage.getHeight() / 2;
AffineTransform tx =
AffineTransform.getRotateInstance(rotationRequired, locationX, locationY);
AffineTransformOp rotateOp = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR);
destinationImage = rotateOp.filter(destinationImage, null);
// Apply a blur effect based on the "scanned-ness" // Apply a blur effect based on the "scanned-ness"
float blurIntensity = scannedness / 100.0f * 0.2f; // Between 0.0 and 0.2 float blurIntensity = scannedness / 100.0f * 0.2f; // Between 0.0 and 0.2
float[] matrix = { float[] matrix = {
blurIntensity, blurIntensity, blurIntensity, blurIntensity, blurIntensity, blurIntensity,
blurIntensity, blurIntensity, blurIntensity, blurIntensity, blurIntensity, blurIntensity,
blurIntensity, blurIntensity, blurIntensity blurIntensity, blurIntensity, blurIntensity
}; };
BufferedImageOp blurOp = new ConvolveOp(new Kernel(3, 3, matrix), ConvolveOp.EDGE_NO_OP, null); BufferedImageOp blurOp =
destinationImage = blurOp.filter(destinationImage, null); new ConvolveOp(new Kernel(3, 3, matrix), ConvolveOp.EDGE_NO_OP, null);
destinationImage = blurOp.filter(destinationImage, null);
// Add noise to the image based on the "dirtiness" // Add noise to the image based on the "dirtiness"
Random random = new SecureRandom(); Random random = new SecureRandom();
for (int y = 0; y < destinationImage.getHeight(); y++) { for (int y = 0; y < destinationImage.getHeight(); y++) {
for (int x = 0; x < destinationImage.getWidth(); x++) { for (int x = 0; x < destinationImage.getWidth(); x++) {
if (random.nextInt(100) < dirtiness) { if (random.nextInt(100) < dirtiness) {
// Change the pixel color to black randomly based on the "dirtiness" // Change the pixel color to black randomly based on the "dirtiness"
destinationImage.setRGB(x, y, Color.BLACK.getRGB()); destinationImage.setRGB(x, y, Color.BLACK.getRGB());
} }
} }
} }
// Save the image // Save the image
ImageIO.write(destinationImage, "PNG", new File("scanned-1.png")); ImageIO.write(destinationImage, "PNG", new File("scanned-1.png"));
PDDocument documentOut = new PDDocument();
for (int page = 1; page <= document.getNumberOfPages(); ++page) {
BufferedImage bim = ImageIO.read(new File("scanned-" + page + ".png"));
// Adjust the dimensions of the page
PDPage pdPage = new PDPage(new PDRectangle(bim.getWidth() - 1, bim.getHeight() - 1));
documentOut.addPage(pdPage);
PDDocument documentOut = new PDDocument(); PDImageXObject pdImage = LosslessFactory.createFromImage(documentOut, bim);
for (int page = 1; page <= document.getNumberOfPages(); ++page) PDPageContentStream contentStream = new PDPageContentStream(documentOut, pdPage);
{
BufferedImage bim = ImageIO.read(new File("scanned-" + page + ".png")); // Draw the image with a slight offset and enlarged dimensions
contentStream.drawImage(pdImage, -1, -1, bim.getWidth() + 2, bim.getHeight() + 2);
// Adjust the dimensions of the page contentStream.close();
PDPage pdPage = new PDPage(new PDRectangle(bim.getWidth() - 1, bim.getHeight() - 1)); }
documentOut.addPage(pdPage); ByteArrayOutputStream baos = new ByteArrayOutputStream();
documentOut.save(baos);
PDImageXObject pdImage = LosslessFactory.createFromImage(documentOut, bim); documentOut.close();
PDPageContentStream contentStream = new PDPageContentStream(documentOut, pdPage);
// Draw the image with a slight offset and enlarged dimensions
contentStream.drawImage(pdImage, -1, -1, bim.getWidth() + 2, bim.getHeight() + 2);
contentStream.close();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
documentOut.save(baos);
documentOut.close();
// Return the optimized PDF as a response // Return the optimized PDF as a response
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scanned.pdf"; String outputFilename =
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scanned.pdf";
return WebResponseUtils.boasToWebResponse(baos, outputFilename); return WebResponseUtils.boasToWebResponse(baos, outputFilename);
} }
} }

View File

@@ -19,6 +19,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.MetadataRequest; import stirling.software.SPDF.model.api.misc.MetadataRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@@ -27,7 +28,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class MetadataController { public class MetadataController {
private String checkUndefined(String entry) { private String checkUndefined(String entry) {
// Check if the string is "undefined" // Check if the string is "undefined"
if ("undefined".equals(entry)) { if ("undefined".equals(entry)) {
@@ -36,14 +36,16 @@ public class MetadataController {
} }
// Return the original string if it's not "undefined" // Return the original string if it's not "undefined"
return entry; return entry;
} }
@PostMapping(consumes = "multipart/form-data", value = "/update-metadata") @PostMapping(consumes = "multipart/form-data", value = "/update-metadata")
@Operation(summary = "Update metadata of a PDF file", @Operation(
description = "This endpoint allows you to update the metadata of a given PDF file. You can add, modify, or delete standard and custom metadata fields. Input:PDF Output:PDF Type:SISO") summary = "Update metadata of a PDF file",
public ResponseEntity<byte[]> metadata(@ModelAttribute MetadataRequest request) throws IOException { description =
"This endpoint allows you to update the metadata of a given PDF file. You can add, modify, or delete standard and custom metadata fields. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> metadata(@ModelAttribute MetadataRequest request)
throws IOException {
// Extract PDF file from the request object // Extract PDF file from the request object
MultipartFile pdfFile = request.getFileInput(); MultipartFile pdfFile = request.getFileInput();
@@ -61,8 +63,8 @@ public class MetadataController {
// Extract additional custom parameters // Extract additional custom parameters
Map<String, String> allRequestParams = request.getAllRequestParams(); Map<String, String> allRequestParams = request.getAllRequestParams();
if(allRequestParams == null) { if (allRequestParams == null) {
allRequestParams = new java.util.HashMap<String, String>(); allRequestParams = new java.util.HashMap<String, String>();
} }
// Load the PDF file into a PDDocument // Load the PDF file into a PDDocument
PDDocument document = PDDocument.load(pdfFile.getBytes()); PDDocument document = PDDocument.load(pdfFile.getBytes());
@@ -89,7 +91,9 @@ public class MetadataController {
} }
// Remove metadata from the PDF history // Remove metadata from the PDF history
document.getDocumentCatalog().getCOSObject().removeItem(COSName.getPDFName("Metadata")); document.getDocumentCatalog().getCOSObject().removeItem(COSName.getPDFName("Metadata"));
document.getDocumentCatalog().getCOSObject().removeItem(COSName.getPDFName("PieceInfo")); document.getDocumentCatalog()
.getCOSObject()
.removeItem(COSName.getPDFName("PieceInfo"));
author = null; author = null;
creationDate = null; creationDate = null;
creator = null; creator = null;
@@ -104,9 +108,17 @@ public class MetadataController {
for (Entry<String, String> entry : allRequestParams.entrySet()) { for (Entry<String, String> entry : allRequestParams.entrySet()) {
String key = entry.getKey(); String key = entry.getKey();
// Check if the key is a standard metadata key // Check if the key is a standard metadata key
if (!key.equalsIgnoreCase("Author") && !key.equalsIgnoreCase("CreationDate") && !key.equalsIgnoreCase("Creator") && !key.equalsIgnoreCase("Keywords") if (!key.equalsIgnoreCase("Author")
&& !key.equalsIgnoreCase("modificationDate") && !key.equalsIgnoreCase("Producer") && !key.equalsIgnoreCase("Subject") && !key.equalsIgnoreCase("Title") && !key.equalsIgnoreCase("CreationDate")
&& !key.equalsIgnoreCase("Trapped") && !key.contains("customKey") && !key.contains("customValue")) { && !key.equalsIgnoreCase("Creator")
&& !key.equalsIgnoreCase("Keywords")
&& !key.equalsIgnoreCase("modificationDate")
&& !key.equalsIgnoreCase("Producer")
&& !key.equalsIgnoreCase("Subject")
&& !key.equalsIgnoreCase("Title")
&& !key.equalsIgnoreCase("Trapped")
&& !key.contains("customKey")
&& !key.contains("customValue")) {
info.setCustomMetadataValue(key, entry.getValue()); info.setCustomMetadataValue(key, entry.getValue());
} else if (key.contains("customKey")) { } else if (key.contains("customKey")) {
int number = Integer.parseInt(key.replaceAll("\\D", "")); int number = Integer.parseInt(key.replaceAll("\\D", ""));
@@ -119,7 +131,8 @@ public class MetadataController {
if (creationDate != null && creationDate.length() > 0) { if (creationDate != null && creationDate.length() > 0) {
Calendar creationDateCal = Calendar.getInstance(); Calendar creationDateCal = Calendar.getInstance();
try { try {
creationDateCal.setTime(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(creationDate)); creationDateCal.setTime(
new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(creationDate));
} catch (ParseException e) { } catch (ParseException e) {
e.printStackTrace(); e.printStackTrace();
} }
@@ -130,7 +143,8 @@ public class MetadataController {
if (modificationDate != null && modificationDate.length() > 0) { if (modificationDate != null && modificationDate.length() > 0) {
Calendar modificationDateCal = Calendar.getInstance(); Calendar modificationDateCal = Calendar.getInstance();
try { try {
modificationDateCal.setTime(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(modificationDate)); modificationDateCal.setTime(
new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(modificationDate));
} catch (ParseException e) { } catch (ParseException e) {
e.printStackTrace(); e.printStackTrace();
} }
@@ -147,7 +161,8 @@ public class MetadataController {
info.setTrapped(trapped); info.setTrapped(trapped);
document.setDocumentInformation(info); document.setDocumentInformation(info);
return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_metadata.pdf"); return WebResponseUtils.pdfDocToWebResponse(
document,
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_metadata.pdf");
} }
} }

View File

@@ -26,6 +26,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest; import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@@ -44,14 +45,21 @@ public class OCRController {
if (files == null) { if (files == null) {
return Collections.emptyList(); return Collections.emptyList();
} }
return Arrays.stream(files).filter(file -> file.getName().endsWith(".traineddata")).map(file -> file.getName().replace(".traineddata", "")) return Arrays.stream(files)
.filter(lang -> !lang.equalsIgnoreCase("osd")).collect(Collectors.toList()); .filter(file -> file.getName().endsWith(".traineddata"))
.map(file -> file.getName().replace(".traineddata", ""))
.filter(lang -> !lang.equalsIgnoreCase("osd"))
.collect(Collectors.toList());
} }
@PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf") @PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
@Operation(summary = "Process a PDF file with OCR", @Operation(
description = "This endpoint processes a PDF file using OCR (Optical Character Recognition). Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType, and removeImagesAfter options. Input:PDF Output:PDF Type:SI-Conditional") summary = "Process a PDF file with OCR",
public ResponseEntity<byte[]> processPdfWithOCR(@ModelAttribute ProcessPdfWithOcrRequest request) throws IOException, InterruptedException { description =
"This endpoint processes a PDF file using OCR (Optical Character Recognition). Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType, and removeImagesAfter options. Input:PDF Output:PDF Type:SI-Conditional")
public ResponseEntity<byte[]> processPdfWithOCR(
@ModelAttribute ProcessPdfWithOcrRequest request)
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
List<String> selectedLanguages = request.getLanguages(); List<String> selectedLanguages = request.getLanguages();
Boolean sidecar = request.isSidecar(); Boolean sidecar = request.isSidecar();
@@ -65,16 +73,17 @@ public class OCRController {
if (selectedLanguages == null || selectedLanguages.isEmpty()) { if (selectedLanguages == null || selectedLanguages.isEmpty()) {
throw new IOException("Please select at least one language."); throw new IOException("Please select at least one language.");
} }
if(!ocrRenderType.equals("hocr") && !ocrRenderType.equals("sandwich")) { if (!ocrRenderType.equals("hocr") && !ocrRenderType.equals("sandwich")) {
throw new IOException("ocrRenderType wrong"); throw new IOException("ocrRenderType wrong");
} }
// Get available Tesseract languages // Get available Tesseract languages
List<String> availableLanguages = getAvailableTesseractLanguages(); List<String> availableLanguages = getAvailableTesseractLanguages();
// Validate selected languages // Validate selected languages
selectedLanguages = selectedLanguages.stream().filter(availableLanguages::contains).toList(); selectedLanguages =
selectedLanguages.stream().filter(availableLanguages::contains).toList();
if (selectedLanguages.isEmpty()) { if (selectedLanguages.isEmpty()) {
throw new IOException("None of the selected languages are valid."); throw new IOException("None of the selected languages are valid.");
@@ -92,8 +101,16 @@ public class OCRController {
// Run OCR Command // Run OCR Command
String languageOption = String.join("+", selectedLanguages); String languageOption = String.join("+", selectedLanguages);
List<String> command =
List<String> command = new ArrayList<>(Arrays.asList("ocrmypdf", "--verbose", "2", "--output-type", "pdf", "--pdf-renderer" , ocrRenderType)); new ArrayList<>(
Arrays.asList(
"ocrmypdf",
"--verbose",
"2",
"--output-type",
"pdf",
"--pdf-renderer",
ocrRenderType));
if (sidecar != null && sidecar) { if (sidecar != null && sidecar) {
sidecarTextPath = Files.createTempFile("sidecar", ".txt"); sidecarTextPath = Files.createTempFile("sidecar", ".txt");
@@ -120,42 +137,61 @@ public class OCRController {
} }
} }
command.addAll(Arrays.asList("--language", languageOption, tempInputFile.toString(), tempOutputFile.toString())); command.addAll(
Arrays.asList(
"--language",
languageOption,
tempInputFile.toString(),
tempOutputFile.toString()));
// Run CLI command // Run CLI command
ProcessExecutorResult result = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command); ProcessExecutorResult result =
if(result.getRc() != 0 && result.getMessages().contains("multiprocessing/synchronize.py") && result.getMessages().contains("OSError: [Errno 38] Function not implemented")) { ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
command.add("--jobs"); .runCommandWithOutputHandling(command);
command.add("1"); if (result.getRc() != 0
result = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command); && 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);
} }
// Remove images from the OCR processed PDF if the flag is set to true // Remove images from the OCR processed PDF if the flag is set to true
if (removeImagesAfter != null && removeImagesAfter) { if (removeImagesAfter != null && removeImagesAfter) {
Path tempPdfWithoutImages = Files.createTempFile("output_", "_no_images.pdf"); Path tempPdfWithoutImages = Files.createTempFile("output_", "_no_images.pdf");
List<String> gsCommand = Arrays.asList("gs", "-sDEVICE=pdfwrite", "-dFILTERIMAGE", "-o", tempPdfWithoutImages.toString(), tempOutputFile.toString()); List<String> gsCommand =
Arrays.asList(
"gs",
"-sDEVICE=pdfwrite",
"-dFILTERIMAGE",
"-o",
tempPdfWithoutImages.toString(),
tempOutputFile.toString());
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(gsCommand); ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
.runCommandWithOutputHandling(gsCommand);
tempOutputFile = tempPdfWithoutImages; tempOutputFile = tempPdfWithoutImages;
} }
// Read the OCR processed PDF file // Read the OCR processed PDF file
byte[] pdfBytes = Files.readAllBytes(tempOutputFile); byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
// Clean up the temporary files // Clean up the temporary files
Files.delete(tempInputFile); Files.delete(tempInputFile);
// Return the OCR processed PDF as a response // Return the OCR processed PDF as a response
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.pdf"; String outputFilename =
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.pdf";
if (sidecar != null && sidecar) { if (sidecar != null && sidecar) {
// Create a zip file containing both the PDF and the text file // Create a zip file containing both the PDF and the text file
String outputZipFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.zip"; String outputZipFilename =
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.zip";
Path tempZipFile = Files.createTempFile("output_", ".zip"); Path tempZipFile = Files.createTempFile("output_", ".zip");
try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) { try (ZipOutputStream zipOut =
new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) {
// Add PDF file to the zip // Add PDF file to the zip
ZipEntry pdfEntry = new ZipEntry(outputFilename); ZipEntry pdfEntry = new ZipEntry(outputFilename);
zipOut.putNextEntry(pdfEntry); zipOut.putNextEntry(pdfEntry);
@@ -177,13 +213,12 @@ public class OCRController {
Files.delete(sidecarTextPath); Files.delete(sidecarTextPath);
// Return the zip file containing both the PDF and the text file // Return the zip file containing both the PDF and the text file
return WebResponseUtils.bytesToWebResponse(zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.bytesToWebResponse(
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
} else { } else {
// Return the OCR processed PDF as a response // Return the OCR processed PDF as a response
Files.delete(tempOutputFile); Files.delete(tempOutputFile);
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
} }
} }
} }

View File

@@ -14,6 +14,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.OverlayImageRequest; import stirling.software.SPDF.model.api.misc.OverlayImageRequest;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@@ -27,9 +28,9 @@ public class OverlayImageController {
@PostMapping(consumes = "multipart/form-data", value = "/add-image") @PostMapping(consumes = "multipart/form-data", value = "/add-image")
@Operation( @Operation(
summary = "Overlay image onto a PDF file", summary = "Overlay image onto a PDF file",
description = "This endpoint overlays an image onto a PDF file at the specified coordinates. The image can be overlaid on every page of the PDF if specified. Input:PDF/IMAGE Output:PDF Type:MF-SISO" description =
) "This endpoint overlays an image onto a PDF file at the specified coordinates. The image can be overlaid on every page of the PDF if specified. Input:PDF/IMAGE Output:PDF Type:MF-SISO")
public ResponseEntity<byte[]> overlayImage(@ModelAttribute OverlayImageRequest request) { public ResponseEntity<byte[]> overlayImage(@ModelAttribute OverlayImageRequest request) {
MultipartFile pdfFile = request.getFileInput(); MultipartFile pdfFile = request.getFileInput();
MultipartFile imageFile = request.getImageFile(); MultipartFile imageFile = request.getImageFile();
@@ -41,7 +42,9 @@ public class OverlayImageController {
byte[] imageBytes = imageFile.getBytes(); byte[] imageBytes = imageFile.getBytes();
byte[] result = PdfUtils.overlayImage(pdfBytes, imageBytes, x, y, everyPage); byte[] result = PdfUtils.overlayImage(pdfBytes, imageBytes, x, y, everyPage);
return WebResponseUtils.bytesToWebResponse(result, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf"); return WebResponseUtils.bytesToWebResponse(
result,
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf");
} catch (IOException e) { } catch (IOException e) {
logger.error("Failed to add image to PDF", e); logger.error("Failed to add image to PDF", e);
return new ResponseEntity<>(HttpStatus.BAD_REQUEST); return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

View File

@@ -1,135 +1,150 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.AddPageNumbersRequest;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.model.api.misc.AddPageNumbersRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/misc") @RestController
@Tag(name = "Misc", description = "Miscellaneous APIs") @RequestMapping("/api/v1/misc")
public class PageNumbersController { @Tag(name = "Misc", description = "Miscellaneous APIs")
public class PageNumbersController {
private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class);
private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class);
@PostMapping(value = "/add-page-numbers", consumes = "multipart/form-data")
@Operation(summary = "Add page numbers to a PDF document", description = "This operation takes an input PDF file and adds page numbers to it. Input:PDF Output:PDF Type:SISO") @PostMapping(value = "/add-page-numbers", consumes = "multipart/form-data")
public ResponseEntity<byte[]> addPageNumbers(@ModelAttribute AddPageNumbersRequest request) throws IOException { @Operation(
MultipartFile file = request.getFileInput(); summary = "Add page numbers to a PDF document",
String customMargin = request.getCustomMargin(); description =
int position = request.getPosition(); "This operation takes an input PDF file and adds page numbers to it. Input:PDF Output:PDF Type:SISO")
int startingNumber = request.getStartingNumber(); public ResponseEntity<byte[]> addPageNumbers(@ModelAttribute AddPageNumbersRequest request)
String pagesToNumber = request.getPagesToNumber(); throws IOException {
String customText = request.getCustomText(); MultipartFile file = request.getFileInput();
int pageNumber = startingNumber; String customMargin = request.getCustomMargin();
byte[] fileBytes = file.getBytes(); int position = request.getPosition();
PDDocument document = PDDocument.load(fileBytes); int startingNumber = request.getStartingNumber();
String pagesToNumber = request.getPagesToNumber();
float marginFactor; String customText = request.getCustomText();
switch (customMargin.toLowerCase()) { int pageNumber = startingNumber;
case "small": byte[] fileBytes = file.getBytes();
marginFactor = 0.02f; PDDocument document = PDDocument.load(fileBytes);
break;
case "medium": float marginFactor;
marginFactor = 0.035f; switch (customMargin.toLowerCase()) {
break; case "small":
case "large": marginFactor = 0.02f;
marginFactor = 0.05f; break;
break; case "medium":
case "x-large": marginFactor = 0.035f;
marginFactor = 0.075f; break;
break; case "large":
marginFactor = 0.05f;
break;
default: case "x-large":
marginFactor = 0.035f; marginFactor = 0.075f;
break; break;
}
default:
float fontSize = 12.0f; marginFactor = 0.035f;
PDType1Font font = PDType1Font.HELVETICA; break;
if(pagesToNumber == null || pagesToNumber.length() == 0) { }
pagesToNumber = "all";
} float fontSize = 12.0f;
if(customText == null || customText.length() == 0) { PDType1Font font = PDType1Font.HELVETICA;
customText = "{n}"; if (pagesToNumber == null || pagesToNumber.length() == 0) {
} pagesToNumber = "all";
List<Integer> pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages()); }
if (customText == null || customText.length() == 0) {
for (int i : pagesToNumberList) { customText = "{n}";
PDPage page = document.getPage(i); }
PDRectangle pageSize = page.getMediaBox(); List<Integer> pagesToNumberList =
GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages());
String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(document.getNumberOfPages())).replace("{filename}", file.getOriginalFilename().replaceFirst("[.][^.]+$", "")) : String.valueOf(pageNumber);
for (int i : pagesToNumberList) {
float x, y; PDPage page = document.getPage(i);
PDRectangle pageSize = page.getMediaBox();
int xGroup = (position - 1) % 3;
int yGroup = 2 - (position - 1) / 3; String text =
customText != null
switch (xGroup) { ? customText
case 0: // left .replace("{n}", String.valueOf(pageNumber))
x = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth(); .replace("{total}", String.valueOf(document.getNumberOfPages()))
break; .replace(
case 1: // center "{filename}",
x = pageSize.getLowerLeftX() + (pageSize.getWidth() / 2); file.getOriginalFilename()
break; .replaceFirst("[.][^.]+$", ""))
default: // right : String.valueOf(pageNumber);
x = pageSize.getUpperRightX() - marginFactor * pageSize.getWidth();
break; float x, y;
}
int xGroup = (position - 1) % 3;
switch (yGroup) { int yGroup = 2 - (position - 1) / 3;
case 0: // bottom
y = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight(); switch (xGroup) {
break; case 0: // left
case 1: // middle x = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth();
y = pageSize.getLowerLeftY() + (pageSize.getHeight() / 2); break;
break; case 1: // center
default: // top x = pageSize.getLowerLeftX() + (pageSize.getWidth() / 2);
y = pageSize.getUpperRightY() - marginFactor * pageSize.getHeight(); break;
break; default: // right
} x = pageSize.getUpperRightX() - marginFactor * pageSize.getWidth();
break;
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true); }
contentStream.beginText();
contentStream.setFont(font, fontSize); switch (yGroup) {
contentStream.newLineAtOffset(x, y); case 0: // bottom
contentStream.showText(text); y = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight();
contentStream.endText(); break;
contentStream.close(); case 1: // middle
y = pageSize.getLowerLeftY() + (pageSize.getHeight() / 2);
pageNumber++; break;
} default: // top
y = pageSize.getUpperRightY() - marginFactor * pageSize.getHeight();
ByteArrayOutputStream baos = new ByteArrayOutputStream(); break;
document.save(baos); }
document.close();
PDPageContentStream contentStream =
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", MediaType.APPLICATION_PDF); new PDPageContentStream(
document, page, PDPageContentStream.AppendMode.APPEND, true);
} contentStream.beginText();
contentStream.setFont(font, fontSize);
contentStream.newLineAtOffset(x, y);
contentStream.showText(text);
} contentStream.endText();
contentStream.close();
pageNumber++;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
document.close();
return WebResponseUtils.bytesToWebResponse(
baos.toByteArray(),
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf",
MediaType.APPLICATION_PDF);
}
}

View File

@@ -17,6 +17,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@@ -31,11 +32,12 @@ public class RepairController {
@PostMapping(consumes = "multipart/form-data", value = "/repair") @PostMapping(consumes = "multipart/form-data", value = "/repair")
@Operation( @Operation(
summary = "Repair a PDF file", summary = "Repair a PDF file",
description = "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response. Input:PDF Output:PDF Type:SISO" description =
) "This endpoint repairs a given PDF file by running Ghostscript command. 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 request) throws IOException, InterruptedException { public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request)
MultipartFile inputFile = request.getFileInput(); throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
// Save the uploaded file to a temporary location // Save the uploaded file to a temporary location
Path tempInputFile = Files.createTempFile("input_", ".pdf"); Path tempInputFile = Files.createTempFile("input_", ".pdf");
inputFile.transferTo(tempInputFile.toFile()); inputFile.transferTo(tempInputFile.toFile());
@@ -50,8 +52,9 @@ public class RepairController {
command.add("-sDEVICE=pdfwrite"); command.add("-sDEVICE=pdfwrite");
command.add(tempInputFile.toString()); command.add(tempInputFile.toString());
ProcessExecutorResult returnCode =
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(command); ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
.runCommandWithOutputHandling(command);
// Read the optimized PDF file // Read the optimized PDF file
byte[] pdfBytes = Files.readAllBytes(tempOutputFile); byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
@@ -61,8 +64,8 @@ public class RepairController {
Files.delete(tempOutputFile); Files.delete(tempOutputFile);
// Return the optimized PDF as a response // Return the optimized PDF as a response
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_repaired.pdf"; String outputFilename =
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_repaired.pdf";
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
} }
} }

View File

@@ -17,47 +17,60 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class ShowJavascript { public class ShowJavascript {
private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class); private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class);
@PostMapping(consumes = "multipart/form-data", value = "/show-javascript") @PostMapping(consumes = "multipart/form-data", value = "/show-javascript")
@Operation(summary = "Grabs all JS from a PDF and returns a single JS file with all code", description = "desc. Input:PDF Output:JS Type:SISO") @Operation(
summary = "Grabs all JS from a PDF and returns a single JS file with all code",
description = "desc. Input:PDF Output:JS Type:SISO")
public ResponseEntity<byte[]> extractHeader(@ModelAttribute PDFFile request) throws Exception { public ResponseEntity<byte[]> extractHeader(@ModelAttribute PDFFile request) throws Exception {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
String script = ""; String script = "";
try (PDDocument document = PDDocument.load(inputFile.getInputStream())) { try (PDDocument document = PDDocument.load(inputFile.getInputStream())) {
if(document.getDocumentCatalog() != null && document.getDocumentCatalog().getNames() != null) {
PDNameTreeNode<PDActionJavaScript> jsTree = document.getDocumentCatalog().getNames().getJavaScript();
if (jsTree != null) {
Map<String, PDActionJavaScript> jsEntries = jsTree.getNames();
for (Map.Entry<String, PDActionJavaScript> entry : jsEntries.entrySet()) {
String name = entry.getKey();
PDActionJavaScript jsAction = entry.getValue();
String jsCodeStr = jsAction.getAction();
script += "// File: " + inputFile.getOriginalFilename() + ", Script: " + name + "\n" + jsCodeStr + "\n";
}
}
}
if (script.isEmpty()) { if (document.getDocumentCatalog() != null
script = "PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript"; && document.getDocumentCatalog().getNames() != null) {
PDNameTreeNode<PDActionJavaScript> jsTree =
document.getDocumentCatalog().getNames().getJavaScript();
if (jsTree != null) {
Map<String, PDActionJavaScript> jsEntries = jsTree.getNames();
for (Map.Entry<String, PDActionJavaScript> entry : jsEntries.entrySet()) {
String name = entry.getKey();
PDActionJavaScript jsAction = entry.getValue();
String jsCodeStr = jsAction.getAction();
script +=
"// File: "
+ inputFile.getOriginalFilename()
+ ", Script: "
+ name
+ "\n"
+ jsCodeStr
+ "\n";
}
}
} }
return WebResponseUtils.bytesToWebResponse(script.getBytes(StandardCharsets.UTF_8), inputFile.getOriginalFilename() + ".js"); if (script.isEmpty()) {
script =
"PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript";
}
return WebResponseUtils.bytesToWebResponse(
script.getBytes(StandardCharsets.UTF_8),
inputFile.getOriginalFilename() + ".js");
} }
} }
} }

View File

@@ -1,9 +1,11 @@
package stirling.software.SPDF.controller.api.pipeline; package stirling.software.SPDF.controller.api.pipeline;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
@@ -17,44 +19,39 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContext;
import stirling.software.SPDF.SPdfApplication; import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.model.ApiEndpoint; import stirling.software.SPDF.model.ApiEndpoint;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
import org.slf4j.Logger;
@Service @Service
public class ApiDocService { public class ApiDocService {
private final Map<String, ApiEndpoint> apiDocumentation = new HashMap<>(); private final Map<String, ApiEndpoint> apiDocumentation = new HashMap<>();
private static final Logger logger = LoggerFactory.getLogger(ApiDocService.class); private static final Logger logger = LoggerFactory.getLogger(ApiDocService.class);
@Autowired @Autowired private ServletContext servletContext;
private ServletContext servletContext;
private String getApiDocsUrl() { private String getApiDocsUrl() {
String contextPath = servletContext.getContextPath(); String contextPath = servletContext.getContextPath();
String port = SPdfApplication.getPort(); String port = SPdfApplication.getPort();
return "http://localhost:"+ port + contextPath + "/v1/api-docs"; return "http://localhost:" + port + contextPath + "/v1/api-docs";
} }
@Autowired(required=false) @Autowired(required = false)
private UserServiceInterface userService; private UserServiceInterface userService;
private String getApiKeyForUser() { private String getApiKeyForUser() {
if(userService == null) if (userService == null) return "";
return ""; return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId());
return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId()); }
}
JsonNode apiDocsJsonRootNode;
JsonNode apiDocsJsonRootNode;
// @EventListener(ApplicationReadyEvent.class)
private synchronized void loadApiDocumentation() {
//@EventListener(ApplicationReadyEvent.class) String apiDocsJson = "";
private synchronized void loadApiDocumentation() {
String apiDocsJson = "";
try { try {
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
String apiKey = getApiKeyForUser(); String apiKey = getApiKeyForUser();
@@ -64,49 +61,52 @@ public class ApiDocService {
HttpEntity<String> entity = new HttpEntity<>(headers); HttpEntity<String> entity = new HttpEntity<>(headers);
RestTemplate restTemplate = new RestTemplate(); RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class); ResponseEntity<String> response =
restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class);
apiDocsJson = response.getBody(); apiDocsJson = response.getBody();
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
apiDocsJsonRootNode = mapper.readTree(apiDocsJson); apiDocsJsonRootNode = mapper.readTree(apiDocsJson);
JsonNode paths = apiDocsJsonRootNode.path("paths"); JsonNode paths = apiDocsJsonRootNode.path("paths");
paths.fields().forEachRemaining(entry -> { paths.fields()
String path = entry.getKey(); .forEachRemaining(
JsonNode pathNode = entry.getValue(); entry -> {
if (pathNode.has("post")) { String path = entry.getKey();
JsonNode postNode = pathNode.get("post"); JsonNode pathNode = entry.getValue();
ApiEndpoint endpoint = new ApiEndpoint(path, postNode); if (pathNode.has("post")) {
apiDocumentation.put(path, endpoint); JsonNode postNode = pathNode.get("post");
} ApiEndpoint endpoint = new ApiEndpoint(path, postNode);
}); apiDocumentation.put(path, endpoint);
}
});
} catch (Exception e) { } catch (Exception e) {
// Handle exceptions // Handle exceptions
logger.error("Error grabbing swagger doc, body result {}", apiDocsJson); logger.error("Error grabbing swagger doc, body result {}", apiDocsJson);
} }
} }
public boolean isValidOperation(String operationName, Map<String, Object> parameters) { public boolean isValidOperation(String operationName, Map<String, Object> parameters) {
if(apiDocumentation.size() == 0) { if (apiDocumentation.size() == 0) {
loadApiDocumentation(); loadApiDocumentation();
} }
if (!apiDocumentation.containsKey(operationName)) { if (!apiDocumentation.containsKey(operationName)) {
return false; return false;
} }
ApiEndpoint endpoint = apiDocumentation.get(operationName); ApiEndpoint endpoint = apiDocumentation.get(operationName);
return endpoint.areParametersValid(parameters); return endpoint.areParametersValid(parameters);
} }
public boolean isMultiInput(String operationName) { public boolean isMultiInput(String operationName) {
if(apiDocsJsonRootNode == null || apiDocumentation.size() == 0) { if (apiDocsJsonRootNode == null || apiDocumentation.size() == 0) {
loadApiDocumentation(); loadApiDocumentation();
} }
if (!apiDocumentation.containsKey(operationName)) { if (!apiDocumentation.containsKey(operationName)) {
return false; return false;
} }
ApiEndpoint endpoint = apiDocumentation.get(operationName); ApiEndpoint endpoint = apiDocumentation.get(operationName);
String description = endpoint.getDescription(); String description = endpoint.getDescription();
Pattern pattern = Pattern.compile("Type:(\\w+)"); Pattern pattern = Pattern.compile("Type:(\\w+)");
Matcher matcher = pattern.matcher(description); Matcher matcher = pattern.matcher(description);
@@ -115,9 +115,8 @@ public class ApiDocService {
return type.startsWith("MI"); return type.startsWith("MI");
} }
return false; return false;
} }
} }
// Model class for API Endpoint // Model class for API Endpoint

View File

@@ -1,117 +1,133 @@
package stirling.software.SPDF.controller.api.pipeline; package stirling.software.SPDF.controller.api.pipeline;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.util.List; import java.util.HashMap;
import java.util.zip.ZipEntry; import java.util.List;
import java.util.zip.ZipOutputStream; import java.util.Map;
import java.util.zip.ZipEntry;
import org.slf4j.Logger; import java.util.zip.ZipOutputStream;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.slf4j.Logger;
import org.springframework.core.io.Resource; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity; import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.core.JsonProcessingException; import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import io.swagger.v3.oas.annotations.tags.Tag; import com.fasterxml.jackson.databind.ObjectMapper;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.PipelineConfig; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.HandleDataRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.PipelineConfig;
@RestController import stirling.software.SPDF.model.api.HandleDataRequest;
@RequestMapping("/api/v1/pipeline") import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Pipeline", description = "Pipeline APIs")
public class PipelineController { @RestController
@RequestMapping("/api/v1/pipeline")
private static final Logger logger = LoggerFactory.getLogger(PipelineController.class); @Tag(name = "Pipeline", description = "Pipeline APIs")
public class PipelineController {
final String watchedFoldersDir = "./pipeline/watchedFolders/";
final String finishedFoldersDir = "./pipeline/finishedFolders/"; private static final Logger logger = LoggerFactory.getLogger(PipelineController.class);
@Autowired
PipelineProcessor processor; final String watchedFoldersDir = "./pipeline/watchedFolders/";
final String finishedFoldersDir = "./pipeline/finishedFolders/";
@Autowired PipelineProcessor processor;
@Autowired
ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
@Autowired @Autowired private ObjectMapper objectMapper;
private ObjectMapper objectMapper;
@PostMapping("/handleData")
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request)
@PostMapping("/handleData") throws JsonMappingException, JsonProcessingException {
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request) throws JsonMappingException, JsonProcessingException { if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) {
if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
return new ResponseEntity<>(HttpStatus.BAD_REQUEST); }
}
MultipartFile[] files = request.getFileInput();
MultipartFile[] files = request.getFileInput(); String jsonString = request.getJson();
String jsonString = request.getJson(); if (files == null) {
if (files == null) { return null;
return null; }
} PipelineConfig config = objectMapper.readValue(jsonString, PipelineConfig.class);
PipelineConfig config = objectMapper.readValue(jsonString, PipelineConfig.class); logger.info("Received POST request to /handleData with {} files", files.length);
logger.info("Received POST request to /handleData with {} files", files.length); try {
try { List<Resource> inputFiles = processor.generateInputFiles(files);
List<Resource> inputFiles = processor.generateInputFiles(files); if (inputFiles == null || inputFiles.size() == 0) {
if(inputFiles == null || inputFiles.size() == 0) { return null;
return null; }
} List<Resource> outputFiles = processor.runPipelineAgainstFiles(inputFiles, config);
List<Resource> outputFiles = processor.runPipelineAgainstFiles(inputFiles, config); if (outputFiles != null && outputFiles.size() == 1) {
if (outputFiles != null && outputFiles.size() == 1) { // If there is only one file, return it directly
// If there is only one file, return it directly Resource singleFile = outputFiles.get(0);
Resource singleFile = outputFiles.get(0); InputStream is = singleFile.getInputStream();
InputStream is = singleFile.getInputStream(); byte[] bytes = new byte[(int) singleFile.contentLength()];
byte[] bytes = new byte[(int) singleFile.contentLength()]; is.read(bytes);
is.read(bytes); is.close();
is.close();
logger.info("Returning single file response...");
logger.info("Returning single file response..."); return WebResponseUtils.bytesToWebResponse(
return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(), bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM);
MediaType.APPLICATION_OCTET_STREAM); } else if (outputFiles == null) {
} else if (outputFiles == null) { return null;
return null; }
}
// Create a ByteArrayOutputStream to hold the zip
// Create a ByteArrayOutputStream to hold the zip ByteArrayOutputStream baos = new ByteArrayOutputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ZipOutputStream zipOut = new ZipOutputStream(baos);
ZipOutputStream zipOut = new ZipOutputStream(baos);
// A map to keep track of filenames and their counts
// Loop through each file and add it to the zip Map<String, Integer> filenameCount = new HashMap<>();
for (Resource file : outputFiles) {
ZipEntry zipEntry = new ZipEntry(file.getFilename()); // Loop through each file and add it to the zip
zipOut.putNextEntry(zipEntry); for (Resource file : outputFiles) {
String originalFilename = file.getFilename();
// Read the file into a byte array String filename = originalFilename;
InputStream is = file.getInputStream();
byte[] bytes = new byte[(int) file.contentLength()]; // Check if the filename already exists, and modify it if necessary
is.read(bytes); if (filenameCount.containsKey(originalFilename)) {
int count = filenameCount.get(originalFilename);
// Write the bytes of the file to the zip String baseName = originalFilename.replaceAll("\\.[^.]*$", "");
zipOut.write(bytes, 0, bytes.length); String extension = originalFilename.replaceAll("^.*\\.", "");
zipOut.closeEntry(); filename = baseName + "(" + count + ")." + extension;
filenameCount.put(originalFilename, count + 1);
is.close(); } else {
} filenameCount.put(originalFilename, 1);
}
zipOut.close();
ZipEntry zipEntry = new ZipEntry(filename);
logger.info("Returning zipped file response..."); zipOut.putNextEntry(zipEntry);
return WebResponseUtils.boasToWebResponse(baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
} catch (Exception e) { // Read the file into a byte array
logger.error("Error handling data: ", e); InputStream is = file.getInputStream();
return null; byte[] bytes = new byte[(int) file.contentLength()];
} is.read(bytes);
}
// Write the bytes of the file to the zip
} zipOut.write(bytes, 0, bytes.length);
zipOut.closeEntry();
is.close();
}
zipOut.close();
logger.info("Returning zipped file response...");
return WebResponseUtils.boasToWebResponse(
baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
} catch (Exception e) {
logger.error("Error handling data: ", e);
return null;
}
}
}

View File

@@ -33,50 +33,48 @@ import stirling.software.SPDF.model.PipelineOperation;
@Service @Service
public class PipelineDirectoryProcessor { public class PipelineDirectoryProcessor {
private static final Logger logger = LoggerFactory.getLogger(PipelineDirectoryProcessor.class); private static final Logger logger = LoggerFactory.getLogger(PipelineDirectoryProcessor.class);
@Autowired @Autowired private ObjectMapper objectMapper;
private ObjectMapper objectMapper; @Autowired private ApiDocService apiDocService;
@Autowired @Autowired private ApplicationProperties applicationProperties;
private ApiDocService apiDocService;
@Autowired
private ApplicationProperties applicationProperties;
final String watchedFoldersDir = "./pipeline/watchedFolders/"; final String watchedFoldersDir = "./pipeline/watchedFolders/";
final String finishedFoldersDir = "./pipeline/finishedFolders/"; final String finishedFoldersDir = "./pipeline/finishedFolders/";
@Autowired @Autowired PipelineProcessor processor;
PipelineProcessor processor;
@Scheduled(fixedRate = 60000) @Scheduled(fixedRate = 60000)
public void scanFolders() { public void scanFolders() {
if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) { if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) {
return; return;
} }
Path watchedFolderPath = Paths.get(watchedFoldersDir); Path watchedFolderPath = Paths.get(watchedFoldersDir);
if (!Files.exists(watchedFolderPath)) { if (!Files.exists(watchedFolderPath)) {
try { try {
Files.createDirectories(watchedFolderPath); Files.createDirectories(watchedFolderPath);
logger.info("Created directory: {}", watchedFolderPath); logger.info("Created directory: {}", watchedFolderPath);
} catch (IOException e) { } catch (IOException e) {
logger.error("Error creating directory: {}", watchedFolderPath, e); logger.error("Error creating directory: {}", watchedFolderPath, e);
return; return;
} }
} }
try (Stream<Path> paths = Files.walk(watchedFolderPath)) { try (Stream<Path> paths = Files.walk(watchedFolderPath)) {
paths.filter(Files::isDirectory).forEach(t -> { paths.filter(Files::isDirectory)
try { .forEach(
if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) { t -> {
handleDirectory(t); try {
} if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) {
} catch (Exception e) { handleDirectory(t);
logger.error("Error handling directory: {}", t, e); }
} } catch (Exception e) {
}); logger.error("Error handling directory: {}", t, e);
} catch (Exception e) { }
logger.error("Error walking through directory: {}", watchedFolderPath, e); });
} } catch (Exception e) {
} logger.error("Error walking through directory: {}", watchedFolderPath, e);
}
}
public void handleDirectory(Path dir) throws IOException { public void handleDirectory(Path dir) throws IOException {
logger.info("Handling directory: {}", dir); logger.info("Handling directory: {}", dir);
Path processingDir = createProcessingDirectory(dir); Path processingDir = createProcessingDirectory(dir);
@@ -113,13 +111,14 @@ public class PipelineDirectoryProcessor {
return objectMapper.readValue(jsonString, PipelineConfig.class); return objectMapper.readValue(jsonString, PipelineConfig.class);
} }
private void processPipelineOperations(Path dir, Path processingDir, Path jsonFile, PipelineConfig config) throws IOException { private void processPipelineOperations(
Path dir, Path processingDir, Path jsonFile, PipelineConfig config) throws IOException {
for (PipelineOperation operation : config.getOperations()) { for (PipelineOperation operation : config.getOperations()) {
validateOperation(operation); validateOperation(operation);
File[] files = collectFilesForProcessing(dir, jsonFile, operation); File[] files = collectFilesForProcessing(dir, jsonFile, operation);
if(files == null || files.length == 0) { if (files == null || files.length == 0) {
logger.debug("No files detected for {} ", dir); logger.debug("No files detected for {} ", dir);
return; return;
} }
List<File> filesToProcess = prepareFilesForProcessing(files, processingDir); List<File> filesToProcess = prepareFilesForProcessing(files, processingDir);
runPipelineAgainstFiles(filesToProcess, config, dir, processingDir); runPipelineAgainstFiles(filesToProcess, config, dir, processingDir);
@@ -132,20 +131,22 @@ public class PipelineDirectoryProcessor {
} }
} }
private File[] collectFilesForProcessing(Path dir, Path jsonFile, PipelineOperation operation) throws IOException { private File[] collectFilesForProcessing(Path dir, Path jsonFile, PipelineOperation operation)
throws IOException {
try (Stream<Path> paths = Files.list(dir)) { try (Stream<Path> paths = Files.list(dir)) {
if ("automated".equals(operation.getParameters().get("fileInput"))) { if ("automated".equals(operation.getParameters().get("fileInput"))) {
return paths.filter(path -> !Files.isDirectory(path) && !path.equals(jsonFile)) return paths.filter(path -> !Files.isDirectory(path) && !path.equals(jsonFile))
.map(Path::toFile) .map(Path::toFile)
.toArray(File[]::new); .toArray(File[]::new);
} else { } else {
String fileInput = (String) operation.getParameters().get("fileInput"); String fileInput = (String) operation.getParameters().get("fileInput");
return new File[]{new File(fileInput)}; return new File[] {new File(fileInput)};
} }
} }
} }
private List<File> prepareFilesForProcessing(File[] files, Path processingDir) throws IOException { private List<File> prepareFilesForProcessing(File[] files, Path processingDir)
throws IOException {
List<File> filesToProcess = new ArrayList<>(); List<File> filesToProcess = new ArrayList<>();
for (File file : files) { for (File file : files) {
Path targetPath = resolveUniqueFilePath(processingDir, file.getName()); Path targetPath = resolveUniqueFilePath(processingDir, file.getName());
@@ -173,27 +174,33 @@ public class PipelineDirectoryProcessor {
if (dotIndex == -1) { if (dotIndex == -1) {
return originalFileName + suffix; return originalFileName + suffix;
} else { } else {
return originalFileName.substring(0, dotIndex) + suffix + originalFileName.substring(dotIndex); return originalFileName.substring(0, dotIndex)
+ suffix
+ originalFileName.substring(dotIndex);
} }
} }
private void runPipelineAgainstFiles(List<File> filesToProcess, PipelineConfig config, Path dir, Path processingDir) throws IOException { private void runPipelineAgainstFiles(
List<File> filesToProcess, PipelineConfig config, Path dir, Path processingDir)
throws IOException {
try { try {
List<Resource> inputFiles = processor.generateInputFiles(filesToProcess.toArray(new File[0])); List<Resource> inputFiles =
if(inputFiles == null || inputFiles.size() == 0) { processor.generateInputFiles(filesToProcess.toArray(new File[0]));
return; if (inputFiles == null || inputFiles.size() == 0) {
return;
} }
List<Resource> outputFiles = processor.runPipelineAgainstFiles(inputFiles, config); List<Resource> outputFiles = processor.runPipelineAgainstFiles(inputFiles, config);
if (outputFiles == null) return; if (outputFiles == null) return;
moveAndRenameFiles(outputFiles, config, dir); moveAndRenameFiles(outputFiles, config, dir);
deleteOriginalFiles(filesToProcess, processingDir); deleteOriginalFiles(filesToProcess, processingDir);
} catch (Exception e) { } catch (Exception e) {
logger.error("error during processing", e); logger.error("error during processing", e);
moveFilesBack(filesToProcess, processingDir); moveFilesBack(filesToProcess, processingDir);
} }
} }
private void moveAndRenameFiles(List<Resource> resources, PipelineConfig config, Path dir) throws IOException { private void moveAndRenameFiles(List<Resource> resources, PipelineConfig config, Path dir)
throws IOException {
for (Resource resource : resources) { for (Resource resource : resources) {
String outputFileName = createOutputFileName(resource, config); String outputFileName = createOutputFileName(resource, config);
Path outputPath = determineOutputPath(config, dir); Path outputPath = determineOutputPath(config, dir);
@@ -217,26 +224,36 @@ public class PipelineDirectoryProcessor {
String baseName = resourceName.substring(0, resourceName.lastIndexOf('.')); String baseName = resourceName.substring(0, resourceName.lastIndexOf('.'));
String extension = resourceName.substring(resourceName.lastIndexOf('.') + 1); String extension = resourceName.substring(resourceName.lastIndexOf('.') + 1);
String outputFileName = config.getOutputPattern() String outputFileName =
.replace("{filename}", baseName) config.getOutputPattern()
.replace("{pipelineName}", config.getName()) .replace("{filename}", baseName)
.replace("{date}", LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))) .replace("{pipelineName}", config.getName())
.replace("{time}", LocalTime.now().format(DateTimeFormatter.ofPattern("HHmmss"))) .replace(
+ "." + extension; "{date}",
LocalDate.now()
.format(DateTimeFormatter.ofPattern("yyyyMMdd")))
.replace(
"{time}",
LocalTime.now()
.format(DateTimeFormatter.ofPattern("HHmmss")))
+ "."
+ extension;
return outputFileName; return outputFileName;
} }
private Path determineOutputPath(PipelineConfig config, Path dir) { private Path determineOutputPath(PipelineConfig config, Path dir) {
String outputDir = config.getOutputDir() String outputDir =
.replace("{outputFolder}", finishedFoldersDir) config.getOutputDir()
.replace("{folderName}", dir.toString()) .replace("{outputFolder}", finishedFoldersDir)
.replaceAll("\\\\?watchedFolders", ""); .replace("{folderName}", dir.toString())
.replaceAll("\\\\?watchedFolders", "");
return Paths.get(outputDir).isAbsolute() ? Paths.get(outputDir) : Paths.get(".", outputDir); return Paths.get(outputDir).isAbsolute() ? Paths.get(outputDir) : Paths.get(".", outputDir);
} }
private void deleteOriginalFiles(List<File> filesToProcess, Path processingDir) throws IOException { private void deleteOriginalFiles(List<File> filesToProcess, Path processingDir)
throws IOException {
for (File file : filesToProcess) { for (File file : filesToProcess) {
Files.deleteIfExists(processingDir.resolve(file.getName())); Files.deleteIfExists(processingDir.resolve(file.getName()));
logger.info("Deleted original file: {}", file.getName()); logger.info("Deleted original file: {}", file.getName());
@@ -247,12 +264,13 @@ public class PipelineDirectoryProcessor {
for (File file : filesToProcess) { for (File file : filesToProcess) {
try { try {
Files.move(processingDir.resolve(file.getName()), file.toPath()); Files.move(processingDir.resolve(file.getName()), file.toPath());
logger.info("Moved file back to original location: {} , {}",file.toPath(), file.getName()); logger.info(
"Moved file back to original location: {} , {}",
file.toPath(),
file.getName());
} catch (IOException e) { } catch (IOException e) {
logger.error("Error moving file back to original location: {}", file.getName(), e); logger.error("Error moving file back to original location: {}", file.getName(), e);
} }
} }
} }
} }

View File

@@ -5,6 +5,8 @@ import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.PrintStream; import java.io.PrintStream;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
@@ -34,7 +36,6 @@ import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContext;
import stirling.software.SPDF.SPdfApplication; import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineOperation;
@@ -43,152 +44,161 @@ import stirling.software.SPDF.model.Role;
@Service @Service
public class PipelineProcessor { public class PipelineProcessor {
private static final Logger logger = LoggerFactory.getLogger(PipelineProcessor.class); private static final Logger logger = LoggerFactory.getLogger(PipelineProcessor.class);
@Autowired private ApiDocService apiDocService;
@Autowired @Autowired(required = false)
private ApiDocService apiDocService;
@Autowired(required=false)
private UserServiceInterface userService; private UserServiceInterface userService;
@Autowired
private ServletContext servletContext;
@Autowired private ServletContext servletContext;
private String getApiKeyForUser() {
if (userService == null) return "";
return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId());
}
private String getApiKeyForUser() { private String getBaseUrl() {
if (userService == null) String contextPath = servletContext.getContextPath();
return ""; String port = SPdfApplication.getPort();
return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId());
}
return "http://localhost:" + port + contextPath + "/";
}
private String getBaseUrl() { List<Resource> runPipelineAgainstFiles(List<Resource> outputFiles, PipelineConfig config)
String contextPath = servletContext.getContextPath(); throws Exception {
String port = SPdfApplication.getPort();
return "http://localhost:" + port + contextPath + "/"; ByteArrayOutputStream logStream = new ByteArrayOutputStream();
} PrintStream logPrintStream = new PrintStream(logStream);
boolean hasErrors = false;
List<Resource> runPipelineAgainstFiles(List<Resource> outputFiles, PipelineConfig config) throws Exception {
ByteArrayOutputStream logStream = new ByteArrayOutputStream(); for (PipelineOperation pipelineOperation : config.getOperations()) {
PrintStream logPrintStream = new PrintStream(logStream); String operation = pipelineOperation.getOperation();
boolean isMultiInputOperation = apiDocService.isMultiInput(operation);
boolean hasErrors = false; logger.info(
"Running operation: {} isMultiInputOperation {}",
operation,
isMultiInputOperation);
Map<String, Object> parameters = pipelineOperation.getParameters();
String inputFileExtension = "";
for (PipelineOperation pipelineOperation : config.getOperations()) { // TODO
String operation = pipelineOperation.getOperation(); // if (operationNode.has("inputFileType")) {
boolean isMultiInputOperation = apiDocService.isMultiInput(operation); // inputFileExtension = operationNode.get("inputFileType").asText();
// } else {
inputFileExtension = ".pdf";
// }
final String finalInputFileExtension = inputFileExtension;
logger.info("Running operation: {} isMultiInputOperation {}", operation, isMultiInputOperation); String url = getBaseUrl() + operation;
Map<String, Object> parameters = pipelineOperation.getParameters();
String inputFileExtension = "";
//TODO
//if (operationNode.has("inputFileType")) {
// inputFileExtension = operationNode.get("inputFileType").asText();
//} else {
inputFileExtension = ".pdf";
//}
final String finalInputFileExtension = inputFileExtension;
String url = getBaseUrl() + operation;
List<Resource> newOutputFiles = new ArrayList<>();
if (!isMultiInputOperation) {
for (Resource file : outputFiles) {
boolean hasInputFileType = false;
if (file.getFilename().endsWith(inputFileExtension)) {
hasInputFileType = true;
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("fileInput", file);
List<Resource> newOutputFiles = new ArrayList<>();
for(Entry<String, Object> entry : parameters.entrySet()) { if (!isMultiInputOperation) {
body.add(entry.getKey(), entry.getValue()); for (Resource file : outputFiles) {
} boolean hasInputFileType = false;
if (file.getFilename().endsWith(inputFileExtension)) {
hasInputFileType = true;
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("fileInput", file);
ResponseEntity<byte[]> response = sendWebRequest(url, body); for (Entry<String, Object> entry : parameters.entrySet()) {
body.add(entry.getKey(), entry.getValue());
}
// If the operation is filter and the response body is null or empty, skip this ResponseEntity<byte[]> response = sendWebRequest(url, body);
// file
if (operation.startsWith("filter-")
&& (response.getBody() == null || response.getBody().length == 0)) {
logger.info("Skipping file due to failing {}", operation);
continue;
}
if (!response.getStatusCode().equals(HttpStatus.OK)) { // If the operation is filter and the response body is null or empty, skip
logPrintStream.println("Error: " + response.getBody()); // this
hasErrors = true; // file
continue; if (operation.startsWith("filter-")
} && (response.getBody() == null || response.getBody().length == 0)) {
processOutputFiles(operation, file.getFilename(), response, newOutputFiles); logger.info("Skipping file due to failing {}", operation);
continue;
} }
if (!hasInputFileType) { if (!response.getStatusCode().equals(HttpStatus.OK)) {
logPrintStream.println( logPrintStream.println("Error: " + response.getBody());
"No files with extension " + inputFileExtension + " found for operation " + operation); hasErrors = true;
hasErrors = true; continue;
} }
processOutputFiles(operation, file.getFilename(), response, newOutputFiles);
}
outputFiles = newOutputFiles; if (!hasInputFileType) {
} logPrintStream.println(
"No files with extension "
+ inputFileExtension
+ " found for operation "
+ operation);
hasErrors = true;
}
}
} else { } else {
// Filter and collect all files that match the inputFileExtension // Filter and collect all files that match the inputFileExtension
List<Resource> matchingFiles = outputFiles.stream() List<Resource> matchingFiles =
.filter(file -> file.getFilename().endsWith(finalInputFileExtension)) outputFiles.stream()
.collect(Collectors.toList()); .filter(
file ->
file.getFilename()
.endsWith(finalInputFileExtension))
.collect(Collectors.toList());
// Check if there are matching files // Check if there are matching files
if (!matchingFiles.isEmpty()) { if (!matchingFiles.isEmpty()) {
// Create a new MultiValueMap for the request body // Create a new MultiValueMap for the request body
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
// Add all matching files to the body // Add all matching files to the body
for (Resource file : matchingFiles) { for (Resource file : matchingFiles) {
body.add("fileInput", file); body.add("fileInput", file);
} }
for(Entry<String, Object> entry : parameters.entrySet()) { for (Entry<String, Object> entry : parameters.entrySet()) {
body.add(entry.getKey(), entry.getValue()); body.add(entry.getKey(), entry.getValue());
} }
ResponseEntity<byte[]> response = sendWebRequest(url, body);
// Handle the response ResponseEntity<byte[]> response = sendWebRequest(url, body);
if (response.getStatusCode().equals(HttpStatus.OK)) {
processOutputFiles(operation, matchingFiles.get(0).getFilename(), response, newOutputFiles);
} else {
// Log error if the response status is not OK
logPrintStream.println("Error in multi-input operation: " + response.getBody());
hasErrors = true;
}
} else {
logPrintStream.println("No files with extension " + inputFileExtension + " found for multi-input operation " + operation);
hasErrors = true;
}
}
logPrintStream.close();
} // Handle the response
if (hasErrors) { if (response.getStatusCode().equals(HttpStatus.OK)) {
logger.error("Errors occurred during processing. Log: {}", logStream.toString()); processOutputFiles(
} operation,
return outputFiles; matchingFiles.get(0).getFilename(),
} response,
newOutputFiles);
} else {
// Log error if the response status is not OK
logPrintStream.println(
"Error in multi-input operation: " + response.getBody());
hasErrors = true;
}
} else {
logPrintStream.println(
"No files with extension "
+ inputFileExtension
+ " found for multi-input operation "
+ operation);
hasErrors = true;
}
}
logPrintStream.close();
outputFiles = newOutputFiles;
}
if (hasErrors) {
logger.error("Errors occurred during processing. Log: {}", logStream.toString());
}
return outputFiles;
}
private ResponseEntity<byte[]> sendWebRequest(String url, MultiValueMap<String, Object> body) {
RestTemplate restTemplate = new RestTemplate();
// Set up headers, including API key
private ResponseEntity<byte[]> sendWebRequest(String url, MultiValueMap<String, Object> body ){
RestTemplate restTemplate = new RestTemplate();
// Set up headers, including API key
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
String apiKey = getApiKeyForUser(); String apiKey = getApiKeyForUser();
headers.add("X-API-Key", apiKey); headers.add("X-API-Key", apiKey);
@@ -199,134 +209,164 @@ public class PipelineProcessor {
// Make the request to the REST endpoint // Make the request to the REST endpoint
return restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class); return restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
} }
private List<Resource> processOutputFiles(String operation, String fileName, ResponseEntity<byte[]> response, List<Resource> newOutputFiles) throws IOException{
// Define filename
String newFilename;
if ("auto-rename".equals(operation)) {
// If the operation is "auto-rename", generate a new filename.
// This is a simple example of generating a filename using current timestamp.
// Modify as per your needs.
newFilename = "file_" + System.currentTimeMillis();
} else {
// Otherwise, keep the original filename.
newFilename = fileName;
}
// Check if the response body is a zip file private List<Resource> processOutputFiles(
if (isZip(response.getBody())) { String operation,
// Unzip the file and add all the files to the new output files String fileName,
newOutputFiles.addAll(unzip(response.getBody())); ResponseEntity<byte[]> response,
} else { List<Resource> newOutputFiles)
Resource outputResource = new ByteArrayResource(response.getBody()) { throws IOException {
@Override // Define filename
public String getFilename() { String newFilename;
return newFilename; if (operation.contains("auto-rename")) {
} // If the operation is "auto-rename", generate a new filename.
}; // This is a simple example of generating a filename using current timestamp.
newOutputFiles.add(outputResource); // Modify as per your needs.
}
return newOutputFiles;
}
List<Resource> generateInputFiles(File[] files) throws Exception {
if (files == null || files.length == 0) {
logger.info("No files");
return null;
}
newFilename = extractFilename(response);
List<Resource> outputFiles = new ArrayList<>(); } else {
// Otherwise, keep the original filename.
newFilename = fileName;
}
for (File file : files) { // Check if the response body is a zip file
Path path = Paths.get(file.getAbsolutePath()); if (isZip(response.getBody())) {
logger.info("Reading file: " + path); // debug statement // Unzip the file and add all the files to the new output files
newOutputFiles.addAll(unzip(response.getBody()));
} else {
Resource outputResource =
new ByteArrayResource(response.getBody()) {
@Override
public String getFilename() {
return newFilename;
}
};
newOutputFiles.add(outputResource);
}
if (Files.exists(path)) { return newOutputFiles;
Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) { }
@Override
public String getFilename() {
return file.getName();
}
};
outputFiles.add(fileResource);
} else {
logger.info("File not found: " + path);
}
}
logger.info("Files successfully loaded. Starting processing...");
return outputFiles;
}
List<Resource> generateInputFiles(MultipartFile[] files) throws Exception { public String extractFilename(ResponseEntity<byte[]> response) {
if (files == null || files.length == 0) { String filename = "default-filename.ext"; // Default filename if not found
logger.info("No files");
return null;
}
List<Resource> outputFiles = new ArrayList<>(); HttpHeaders headers = response.getHeaders();
String contentDisposition = headers.getFirst(HttpHeaders.CONTENT_DISPOSITION);
for (MultipartFile file : files) { if (contentDisposition != null && !contentDisposition.isEmpty()) {
Resource fileResource = new ByteArrayResource(file.getBytes()) { String[] parts = contentDisposition.split(";");
@Override for (String part : parts) {
public String getFilename() { if (part.trim().startsWith("filename")) {
return file.getOriginalFilename(); // Extracts filename and removes quotes if present
} filename = part.split("=")[1].trim().replace("\"", "");
}; filename = URLDecoder.decode(filename, StandardCharsets.UTF_8);
outputFiles.add(fileResource);
}
logger.info("Files successfully loaded. Starting processing...");
return outputFiles;
}
private boolean isZip(byte[] data) { break;
if (data == null || data.length < 4) { }
return false; }
} }
// Check the first four bytes of the data against the standard zip magic number return filename;
return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04; }
}
private List<Resource> unzip(byte[] data) throws IOException { List<Resource> generateInputFiles(File[] files) throws Exception {
logger.info("Unzipping data of length: {}", data.length); if (files == null || files.length == 0) {
List<Resource> unzippedFiles = new ArrayList<>(); logger.info("No files");
return null;
}
try (ByteArrayInputStream bais = new ByteArrayInputStream(data); List<Resource> outputFiles = new ArrayList<>();
ZipInputStream zis = new ZipInputStream(bais)) {
ZipEntry entry; for (File file : files) {
while ((entry = zis.getNextEntry()) != null) { Path path = Paths.get(file.getAbsolutePath());
ByteArrayOutputStream baos = new ByteArrayOutputStream(); logger.info("Reading file: " + path); // debug statement
byte[] buffer = new byte[1024];
int count;
while ((count = zis.read(buffer)) != -1) { if (Files.exists(path)) {
baos.write(buffer, 0, count); Resource fileResource =
} new ByteArrayResource(Files.readAllBytes(path)) {
@Override
public String getFilename() {
return file.getName();
}
};
outputFiles.add(fileResource);
} else {
logger.info("File not found: " + path);
}
}
logger.info("Files successfully loaded. Starting processing...");
return outputFiles;
}
final String filename = entry.getName(); List<Resource> generateInputFiles(MultipartFile[] files) throws Exception {
Resource fileResource = new ByteArrayResource(baos.toByteArray()) { if (files == null || files.length == 0) {
@Override logger.info("No files");
public String getFilename() { return null;
return filename; }
}
};
// If the unzipped file is a zip file, unzip it List<Resource> outputFiles = new ArrayList<>();
if (isZip(baos.toByteArray())) {
logger.info("File {} is a zip file. Unzipping...", filename);
unzippedFiles.addAll(unzip(baos.toByteArray()));
} else {
unzippedFiles.add(fileResource);
}
}
}
logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size()); for (MultipartFile file : files) {
return unzippedFiles; Resource fileResource =
} new ByteArrayResource(file.getBytes()) {
@Override
public String getFilename() {
return file.getOriginalFilename();
}
};
outputFiles.add(fileResource);
}
logger.info("Files successfully loaded. Starting processing...");
return outputFiles;
}
private boolean isZip(byte[] data) {
if (data == null || data.length < 4) {
return false;
}
// Check the first four bytes of the data against the standard zip magic number
return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04;
}
private List<Resource> unzip(byte[] data) throws IOException {
logger.info("Unzipping data of length: {}", data.length);
List<Resource> unzippedFiles = new ArrayList<>();
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ZipInputStream zis = new ZipInputStream(bais)) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int count;
while ((count = zis.read(buffer)) != -1) {
baos.write(buffer, 0, count);
}
final String filename = entry.getName();
Resource fileResource =
new ByteArrayResource(baos.toByteArray()) {
@Override
public String getFilename() {
return filename;
}
};
// If the unzipped file is a zip file, unzip it
if (isZip(baos.toByteArray())) {
logger.info("File {} is a zip file. Unzipping...", filename);
unzippedFiles.addAll(unzip(baos.toByteArray()));
} else {
unzippedFiles.add(fileResource);
}
}
}
logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
return unzippedFiles;
}
} }

View File

@@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.api.pipeline; package stirling.software.SPDF.controller.api.pipeline;
public interface UserServiceInterface { public interface UserServiceInterface {
String getApiKeyForUser(String username); String getApiKeyForUser(String username);
} }

View File

@@ -4,44 +4,34 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.security.KeyFactory; import java.io.OutputStream;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.Security; import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.text.SimpleDateFormat;
import java.util.Calendar; import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import org.apache.commons.io.IOUtils; import org.apache.pdfbox.examples.signature.CreateSignatureBase;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.ExternalSigningSupport;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.CMSTypedData;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.util.io.pem.PemReader; import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.bouncycastle.pkcs.PKCSException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -53,6 +43,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest; import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@@ -61,198 +52,131 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
public class CertSignController { public class CertSignController {
private static final Logger logger = LoggerFactory.getLogger(CertSignController.class); private static final Logger logger = LoggerFactory.getLogger(CertSignController.class);
static { static {
Security.addProvider(new BouncyCastleProvider()); Security.addProvider(new BouncyCastleProvider());
} }
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign") class CreateSignature extends CreateSignatureBase {
@Operation(summary = "Sign PDF with a Digital Certificate", description = "This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:MF-SISO") public CreateSignature(KeyStore keystore, char[] pin)
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) throws Exception { throws KeyStoreException,
MultipartFile pdf = request.getFileInput(); UnrecoverableKeyException,
String certType = request.getCertType(); NoSuchAlgorithmException,
MultipartFile privateKeyFile = request.getPrivateKeyFile(); IOException,
MultipartFile certFile = request.getCertFile(); CertificateException {
MultipartFile p12File = request.getP12File(); super(keystore, pin);
String password = request.getPassword(); }
Boolean showSignature = request.isShowSignature(); }
String reason = request.getReason();
String location = request.getLocation();
String name = request.getName();
Integer pageNumber = request.getPageNumber();
PrivateKey privateKey = null; @PostMapping(consumes = "multipart/form-data", value = "/cert-sign")
X509Certificate cert = null; @Operation(
summary = "Sign PDF with a Digital Certificate",
description =
"This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:MF-SISO")
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request)
throws Exception {
MultipartFile pdf = request.getFileInput();
String certType = request.getCertType();
MultipartFile privateKeyFile = request.getPrivateKeyFile();
MultipartFile certFile = request.getCertFile();
MultipartFile p12File = request.getP12File();
MultipartFile jksfile = request.getJksFile();
String password = request.getPassword();
Boolean showSignature = request.isShowSignature();
String reason = request.getReason();
String location = request.getLocation();
String name = request.getName();
Integer pageNumber = request.getPageNumber();
if (certType != null) { if (certType == null) {
logger.info("Cert type provided: {}", certType); throw new IllegalArgumentException("Cert type must be provided");
switch (certType) { }
case "PKCS12":
if (p12File != null) {
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(new ByteArrayInputStream(p12File.getBytes()), password.toCharArray());
String alias = ks.aliases().nextElement();
if (!ks.isKeyEntry(alias)) {
throw new IllegalArgumentException("The provided PKCS12 file does not contain a private key.");
}
privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray());
cert = (X509Certificate) ks.getCertificate(alias);
}
break;
case "PEM":
if (privateKeyFile != null && certFile != null) {
// Load private key
KeyFactory keyFactory = KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME);
if (isPEM(privateKeyFile.getBytes())) {
privateKey = keyFactory
.generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes())));
} else {
privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes()));
}
// Load certificate KeyStore ks = null;
CertificateFactory certFactory = CertificateFactory.getInstance("X.509",
BouncyCastleProvider.PROVIDER_NAME);
if (isPEM(certFile.getBytes())) {
cert = (X509Certificate) certFactory
.generateCertificate(new ByteArrayInputStream(parsePEM(certFile.getBytes())));
} else {
cert = (X509Certificate) certFactory
.generateCertificate(new ByteArrayInputStream(certFile.getBytes()));
}
}
break;
}
}
PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); // default filter
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_SHA1);
signature.setName(name);
signature.setLocation(location);
signature.setReason(reason);
signature.setSignDate(Calendar.getInstance());
// Load the PDF
try (PDDocument document = PDDocument.load(pdf.getBytes())) {
logger.info("Successfully loaded the provided PDF");
SignatureOptions signatureOptions = new SignatureOptions();
// If you want to show the signature switch (certType) {
case "PEM":
ks = KeyStore.getInstance("JKS");
ks.load(null);
PrivateKey privateKey = getPrivateKeyFromPEM(privateKeyFile.getBytes(), password);
Certificate cert = (Certificate) getCertificateFromPEM(certFile.getBytes());
ks.setKeyEntry(
"alias", privateKey, password.toCharArray(), new Certificate[] {cert});
break;
case "PKCS12":
ks = KeyStore.getInstance("PKCS12");
ks.load(p12File.getInputStream(), password.toCharArray());
break;
case "JKS":
ks = KeyStore.getInstance("JKS");
ks.load(jksfile.getInputStream(), password.toCharArray());
break;
default:
throw new IllegalArgumentException("Invalid cert type: " + certType);
}
// ATTEMPT 2 // TODO: page number
if (showSignature != null && showSignature) {
PDPage page = document.getPage(pageNumber - 1);
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); CreateSignature createSignature = new CreateSignature(ks, password.toCharArray());
if (acroForm == null) { ByteArrayOutputStream baos = new ByteArrayOutputStream();
acroForm = new PDAcroForm(document); sign(pdf.getBytes(), baos, createSignature, name, location, reason);
document.getDocumentCatalog().setAcroForm(acroForm); return WebResponseUtils.boasToWebResponse(
} baos, pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_signed.pdf");
}
// Create a new signature field and widget private static void sign(
byte[] input,
OutputStream output,
CreateSignature instance,
String name,
String location,
String reason) {
try (PDDocument doc = PDDocument.load(input)) {
PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName(name);
signature.setLocation(location);
signature.setReason(reason);
signature.setSignDate(Calendar.getInstance());
PDSignatureField signatureField = new PDSignatureField(acroForm); doc.addSignature(signature, instance);
PDAnnotationWidget widget = signatureField.getWidgets().get(0); doc.saveIncremental(output);
PDRectangle rect = new PDRectangle(100, 100, 200, 50); // Define the rectangle size here } catch (Exception e) {
widget.setRectangle(rect); e.printStackTrace();
page.getAnnotations().add(widget); }
}
// Set the appearance for the signature field private PrivateKey getPrivateKeyFromPEM(byte[] pemBytes, String password)
PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary(); throws IOException, OperatorCreationException, PKCSException {
PDAppearanceStream appearanceStream = new PDAppearanceStream(document); try (PEMParser pemParser =
appearanceStream.setResources(new PDResources()); new PEMParser(new InputStreamReader(new ByteArrayInputStream(pemBytes)))) {
appearanceStream.setBBox(rect); Object pemObject = pemParser.readObject();
appearanceDict.setNormalAppearance(appearanceStream); JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
widget.setAppearance(appearanceDict); PrivateKeyInfo pkInfo;
if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo) {
try (PDPageContentStream contentStream = new PDPageContentStream(document, appearanceStream)) { InputDecryptorProvider decProv =
contentStream.beginText(); new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password.toCharArray());
contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12); pkInfo = ((PKCS8EncryptedPrivateKeyInfo) pemObject).decryptPrivateKeyInfo(decProv);
contentStream.newLineAtOffset(110, 130); } else if (pemObject instanceof PEMEncryptedKeyPair) {
contentStream.showText("Digitally signed by: " + (name != null ? name : "Unknown")); PEMDecryptorProvider decProv =
contentStream.newLineAtOffset(0, -15); new JcePEMDecryptorProviderBuilder().build(password.toCharArray());
contentStream.showText("Date: " + new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z").format(new Date())); pkInfo =
contentStream.newLineAtOffset(0, -15); ((PEMEncryptedKeyPair) pemObject)
if (reason != null && !reason.isEmpty()) { .decryptKeyPair(decProv)
contentStream.showText("Reason: " + reason); .getPrivateKeyInfo();
contentStream.newLineAtOffset(0, -15); } else {
} pkInfo = ((PEMKeyPair) pemObject).getPrivateKeyInfo();
if (location != null && !location.isEmpty()) { }
contentStream.showText("Location: " + location); return converter.getPrivateKey(pkInfo);
contentStream.newLineAtOffset(0, -15); }
} }
contentStream.endText();
}
// Add the widget annotation to the page
page.getAnnotations().add(widget);
// Add the signature field to the acroform
acroForm.getFields().add(signatureField);
// Handle multiple signatures by ensuring a unique field name
String baseFieldName = "Signature";
String signatureFieldName = baseFieldName;
int suffix = 1;
while (acroForm.getField(signatureFieldName) != null) {
suffix++;
signatureFieldName = baseFieldName + suffix;
}
signatureField.setPartialName(signatureFieldName);
}
document.addSignature(signature, signatureOptions);
logger.info("Signature added to the PDF document");
// External signing
ExternalSigningSupport externalSigning = document
.saveIncrementalForExternalSigning(new ByteArrayOutputStream());
byte[] content = IOUtils.toByteArray(externalSigning.getContent());
// Using BouncyCastle to sign
CMSTypedData cmsData = new CMSProcessableByteArray(content);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
.setProvider(BouncyCastleProvider.PROVIDER_NAME).build(privateKey);
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build())
.build(signer, cert));
gen.addCertificates(new JcaCertStore(Collections.singletonList(cert)));
CMSSignedData signedData = gen.generate(cmsData, false);
byte[] cmsSignature = signedData.getEncoded();
logger.info("About to sign content using BouncyCastle");
externalSigning.setSignature(cmsSignature);
logger.info("Signature set successfully");
// After setting the signature, return the resultant PDF
try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) {
document.save(signedPdfOutput);
return WebResponseUtils.boasToWebResponse(signedPdfOutput,
pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_signed.pdf");
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private byte[] parsePEM(byte[] content) throws IOException {
PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(content)));
return pemReader.readPemObject().getContent();
}
private boolean isPEM(byte[] content) {
String contentStr = new String(content);
return contentStr.contains("-----BEGIN") && contentStr.contains("-----END");
}
private Certificate getCertificateFromPEM(byte[] pemBytes)
throws IOException, CertificateException {
try (ByteArrayInputStream bis = new ByteArrayInputStream(pemBytes)) {
return CertificateFactory.getInstance("X.509").generateCertificate(bis);
}
}
} }

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