Compare commits
218 Commits
v0.16.0
...
add-elemen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
217bba3a9d | ||
|
|
7572db9bd4 | ||
|
|
9e3b50dff3 | ||
|
|
cf640c7e3f | ||
|
|
ec770e1008 | ||
|
|
15e0048bfc | ||
|
|
b572a5e4c9 | ||
|
|
c55a5657a4 | ||
|
|
5f771b7851 | ||
|
|
b58cbdcb61 | ||
|
|
9e81667ecd | ||
|
|
a92479b505 | ||
|
|
279cfa70f5 | ||
|
|
f8ad71aa4e | ||
|
|
524d198212 | ||
|
|
1baf458344 | ||
|
|
da50e4d212 | ||
|
|
bbd8de0899 | ||
|
|
c548aa037e | ||
|
|
6a7ed615e3 | ||
|
|
56cbb4381b | ||
|
|
4612b05199 | ||
|
|
7b43fca6fc | ||
|
|
e3c8af7e54 | ||
|
|
63eacf443e | ||
|
|
b32c28e9cb | ||
|
|
a5ee10e029 | ||
|
|
bb1d41d74a | ||
|
|
0698e2888d | ||
|
|
e1f0a6cb1d | ||
|
|
7fecae8b0d | ||
|
|
e6622dfdc4 | ||
|
|
34b4ae0e03 | ||
|
|
036fd711f9 | ||
|
|
80a59205fa | ||
|
|
cbe4bca716 | ||
|
|
232a556425 | ||
|
|
a497ad8c41 | ||
|
|
3041e80c37 | ||
|
|
1b2df20fdd | ||
|
|
0e69f7e0e8 | ||
|
|
94aba370e0 | ||
|
|
cd49d7ffa2 | ||
|
|
dc297644d1 | ||
|
|
a7b4e44e6d | ||
|
|
610ff22abe | ||
|
|
27e8335f79 | ||
|
|
168a0f001c | ||
|
|
5c6936b494 | ||
|
|
a715dbb25d | ||
|
|
a43e13cf94 | ||
|
|
7f805d16a1 | ||
|
|
6f3cbe0cae | ||
|
|
0e9f52bca2 | ||
|
|
44e3556382 | ||
|
|
4f6286845d | ||
|
|
c5ba546a02 | ||
|
|
d63519bbf4 | ||
|
|
aeadc88f92 | ||
|
|
829e98c29b | ||
|
|
3e3f4a0188 | ||
|
|
e5bdd52b7c | ||
|
|
e653ef6522 | ||
|
|
5fcb4e893b | ||
|
|
c2b524e459 | ||
|
|
5a055bae5e | ||
|
|
c3f501d701 | ||
|
|
8acab77ae3 | ||
|
|
8bd2784f37 | ||
|
|
e2c5027311 | ||
|
|
d2b2adcbc1 | ||
|
|
48158379ee | ||
|
|
6ba84a190f | ||
|
|
d349aea1be | ||
|
|
79e2683cbe | ||
|
|
e4fb64ce16 | ||
|
|
d405b7a810 | ||
|
|
1d243a0ca5 | ||
|
|
1f10693eaf | ||
|
|
b7f62a635d | ||
|
|
4e991e7ec2 | ||
|
|
8fe7e57a6a | ||
|
|
5c79a5da29 | ||
|
|
d01473aceb | ||
|
|
3911be0177 | ||
|
|
78da44ad83 | ||
|
|
54859ac3ba | ||
|
|
9cb8c9f655 | ||
|
|
dfda474ba5 | ||
|
|
43f15b3e55 | ||
|
|
86c45f6f8f | ||
|
|
de83321c62 | ||
|
|
7b44cf77d6 | ||
|
|
c769a02982 | ||
|
|
aa671b8bd6 | ||
|
|
6e7c066e57 | ||
|
|
78ac9231c5 | ||
|
|
5d611a2fa3 | ||
|
|
e9947da5b4 | ||
|
|
8df7dfc3be | ||
|
|
d79db6f3da | ||
|
|
84aebe3851 | ||
|
|
f5c285a70f | ||
|
|
2d6bf43bdb | ||
|
|
964f22e3e0 | ||
|
|
d325020e22 | ||
|
|
aec85ddd66 | ||
|
|
32b009b11f | ||
|
|
2e5b72e4fb | ||
|
|
1d3cf2bdc3 | ||
|
|
2a5fe2bd74 | ||
|
|
61ff0248da | ||
|
|
659af2089c | ||
|
|
8960313a2b | ||
|
|
6ee8e1e37f | ||
|
|
05977aa3a6 | ||
|
|
f7dbb8d0a6 | ||
|
|
eaf65d7981 | ||
|
|
a10e3a025b | ||
|
|
4d3e442ecc | ||
|
|
49576c0aa4 | ||
|
|
960af83f11 | ||
|
|
cf42ef7faa | ||
|
|
d894937c22 | ||
|
|
8938e86223 | ||
|
|
c1a39e53dc | ||
|
|
cf3693186a | ||
|
|
0fb0cb8bca | ||
|
|
fb18d0d04d | ||
|
|
779d9028fe | ||
|
|
f2b701e3e3 | ||
|
|
a138d5f5a9 | ||
|
|
6276f028ac | ||
|
|
b962e867d8 | ||
|
|
a286a92ede | ||
|
|
7fb8f5ed28 | ||
|
|
03d3235e1d | ||
|
|
d23551857c | ||
|
|
dd9dd72f35 | ||
|
|
9652f59ae9 | ||
|
|
3469beb5b3 | ||
|
|
690720f4e3 | ||
|
|
491be75e1f | ||
|
|
a868b2c649 | ||
|
|
0b49993d80 | ||
|
|
995a926e35 | ||
|
|
914dd0a21a | ||
|
|
d9b5d08b06 | ||
|
|
344d1163ff | ||
|
|
3f50979d3e | ||
|
|
c681f48459 | ||
|
|
2f5d7ed712 | ||
|
|
1efefcfcb8 | ||
|
|
909c9ed4d9 | ||
|
|
116b3535ee | ||
|
|
b7d6107a2d | ||
|
|
120b017b1a | ||
|
|
24568f4a42 | ||
|
|
03450454c5 | ||
|
|
7e982e125d | ||
|
|
e725451530 | ||
|
|
d7d6bc8108 | ||
|
|
6d66ac0a8b | ||
|
|
93f12d1313 | ||
|
|
eab9e3cffc | ||
|
|
d74c25e678 | ||
|
|
c729b7201f | ||
|
|
beab9932d7 | ||
|
|
65fcf29fd5 | ||
|
|
73007239ee | ||
|
|
816d874ac4 | ||
|
|
b66f86f7cc | ||
|
|
168ef747de | ||
|
|
ad047ab012 | ||
|
|
1ea3fb209b | ||
|
|
875d9da36b | ||
|
|
b21d2ecbd1 | ||
|
|
3bffc1da76 | ||
|
|
631d3948bd | ||
|
|
5774a22b64 | ||
|
|
39345bb6bb | ||
|
|
57b483047e | ||
|
|
0fb7633da8 | ||
|
|
dae2f33772 | ||
|
|
31ac877612 | ||
|
|
79dcf99cce | ||
|
|
c28a40ffe8 | ||
|
|
12dccab460 | ||
|
|
0a26e2e6d6 | ||
|
|
74f6cd63f4 | ||
|
|
e8de5739fa | ||
|
|
1b2734d99c | ||
|
|
206cf40cb5 | ||
|
|
78473e96fd | ||
|
|
b7d6ac2cc3 | ||
|
|
4068d9530f | ||
|
|
8a331956c2 | ||
|
|
1d3e018a56 | ||
|
|
3602034938 | ||
|
|
eb4e2d5fca | ||
|
|
b6671939e5 | ||
|
|
41d09e40a1 | ||
|
|
9b0dba7f65 | ||
|
|
ac0dc8b5c7 | ||
|
|
723216c693 | ||
|
|
46f9a5057f | ||
|
|
8a2633ca93 | ||
|
|
a03470d2de | ||
|
|
ef7c98e5cb | ||
|
|
c9cd1331d2 | ||
|
|
ddc14517b8 | ||
|
|
578aecf977 | ||
|
|
b1ca938053 | ||
|
|
298fe349c1 | ||
|
|
f4364a3f33 | ||
|
|
e0f068bc9d | ||
|
|
86984f2142 | ||
|
|
e998426b3b |
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Formatting
|
||||||
|
5f771b785130154ed47952635b7acef371ffe0ec
|
||||||
2
.github/CODEOWNERS
vendored
Normal file
2
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# All PRs to V1 must be approved by Frooodle
|
||||||
|
* @Frooodle
|
||||||
34
.github/workflows/build.yml
vendored
Normal file
34
.github/workflows/build.yml
vendored
Normal 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
|
||||||
55
.github/workflows/codeql.yml
vendored
55
.github/workflows/codeql.yml
vendored
@@ -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
|
|
||||||
2
.github/workflows/push-docker.yml
vendored
2
.github/workflows/push-docker.yml
vendored
@@ -139,6 +139,8 @@ jobs:
|
|||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
tags: ${{ steps.meta3.outputs.tags }}
|
tags: ${{ steps.meta3.outputs.tags }}
|
||||||
labels: ${{ steps.meta3.outputs.labels }}
|
labels: ${{ steps.meta3.outputs.labels }}
|
||||||
|
build-args:
|
||||||
|
VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
- name: Build and Push Helm Chart
|
- name: Build and Push Helm Chart
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -15,8 +15,8 @@ local.properties
|
|||||||
.classpath
|
.classpath
|
||||||
.project
|
.project
|
||||||
version.properties
|
version.properties
|
||||||
pipeline/
|
pipeline/watchedFolders/
|
||||||
|
pipeline/finishedFolders/
|
||||||
#### Stirling-PDF Files ###
|
#### Stirling-PDF Files ###
|
||||||
customFiles/
|
customFiles/
|
||||||
configs/
|
configs/
|
||||||
@@ -120,4 +120,8 @@ watchedFolders/
|
|||||||
/build
|
/build
|
||||||
|
|
||||||
/.vscode
|
/.vscode
|
||||||
/.idea
|
/.idea
|
||||||
|
|
||||||
|
# Ignore Mac DS_Store files
|
||||||
|
.DS_Store
|
||||||
|
**/.DS_Store
|
||||||
12
Dockerfile
12
Dockerfile
@@ -1,12 +1,13 @@
|
|||||||
# Use the base image
|
# Use the base image
|
||||||
FROM frooodle/stirling-pdf-base:version6
|
FROM frooodle/stirling-pdf-base:version8
|
||||||
|
|
||||||
ARG VERSION_TAG
|
ARG VERSION_TAG
|
||||||
|
|
||||||
# Set Environment Variables
|
# Set Environment Variables
|
||||||
ENV DOCKER_ENABLE_SECURITY=false \
|
ENV DOCKER_ENABLE_SECURITY=false \
|
||||||
HOME=/home/stirlingpdfuser \
|
HOME=/home/stirlingpdfuser \
|
||||||
VERSION_TAG=$VERSION_TAG
|
VERSION_TAG=$VERSION_TAG \
|
||||||
|
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
|
||||||
# PUID=1000 \
|
# PUID=1000 \
|
||||||
# PGID=1000 \
|
# PGID=1000 \
|
||||||
# UMASK=022 \
|
# UMASK=022 \
|
||||||
@@ -18,19 +19,20 @@ ENV DOCKER_ENABLE_SECURITY=false \
|
|||||||
## mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
|
## mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
|
||||||
|
|
||||||
# Set up necessary directories and permissions
|
# Set up necessary directories and permissions
|
||||||
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /customFiles
|
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /logs /customFiles /pipeline /pipeline/defaultWebUIConfigs /pipeline/watchedFolders /pipeline/finishedFolders
|
||||||
##&& \
|
##&& \
|
||||||
## chown -R stirlingpdfuser:stirlingpdfgroup /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /customFiles && \
|
## chown -R stirlingpdfuser:stirlingpdfgroup /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /customFiles && \
|
||||||
## chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/tesseract-ocr-original
|
## chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/tesseract-ocr-original
|
||||||
|
|
||||||
# Copy necessary files
|
# Copy necessary files
|
||||||
COPY ./scripts/* /scripts/
|
COPY ./scripts/* /scripts/
|
||||||
|
COPY ./pipeline/ /pipeline/
|
||||||
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
||||||
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
|
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
|
||||||
COPY build/libs/*.jar app.jar
|
COPY build/libs/*.jar app.jar
|
||||||
|
|
||||||
# Set font cache and permissions
|
# Set font cache and permissions
|
||||||
RUN fc-cache -f -v && chmod +x /scripts/init.sh
|
RUN fc-cache -f -v && chmod +x /scripts/*
|
||||||
|
|
||||||
##&& \
|
##&& \
|
||||||
## chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
|
## chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
|
||||||
@@ -42,4 +44,4 @@ EXPOSE 8080
|
|||||||
# Set user and run command
|
# Set user and run command
|
||||||
##USER stirlingpdfuser
|
##USER stirlingpdfuser
|
||||||
ENTRYPOINT ["/scripts/init.sh"]
|
ENTRYPOINT ["/scripts/init.sh"]
|
||||||
CMD ["java", "-jar", "/app.jar"]
|
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
# Build jbig2enc in a separate stage
|
# Build jbig2enc in a separate stage
|
||||||
FROM bellsoft/liberica-openjdk-debian:17
|
FROM bellsoft/liberica-openjdk-debian:17
|
||||||
|
|
||||||
|
ARG VERSION_TAG
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
libreoffice-core-nogui \
|
libreoffice-core \
|
||||||
libreoffice-common \
|
libreoffice-common \
|
||||||
libreoffice-writer-nogui \
|
libreoffice-writer \
|
||||||
libreoffice-calc-nogui \
|
libreoffice-calc \
|
||||||
libreoffice-impress-nogui \
|
libreoffice-impress \
|
||||||
unoconv && \
|
unoconv && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
@@ -14,7 +17,8 @@ RUN apt-get update && \
|
|||||||
# Set Environment Variables
|
# Set Environment Variables
|
||||||
ENV DOCKER_ENABLE_SECURITY=false \
|
ENV DOCKER_ENABLE_SECURITY=false \
|
||||||
HOME=/home/stirlingpdfuser \
|
HOME=/home/stirlingpdfuser \
|
||||||
VERSION_TAG=$VERSION_TAG
|
VERSION_TAG=$VERSION_TAG \
|
||||||
|
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
|
||||||
# PUID=1000 \
|
# PUID=1000 \
|
||||||
# PGID=1000 \
|
# PGID=1000 \
|
||||||
# UMASK=022 \
|
# UMASK=022 \
|
||||||
@@ -25,17 +29,24 @@ ENV DOCKER_ENABLE_SECURITY=false \
|
|||||||
# mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
|
# mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
|
||||||
|
|
||||||
# Set up necessary directories and permissions
|
# Set up necessary directories and permissions
|
||||||
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /configs /customFiles
|
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /configs /customFiles /logs /pipeline /pipeline/defaultWebUIConfigs /pipeline/watchedFolders /pipeline/finishedFolders
|
||||||
|
|
||||||
# chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/fonts/opentype/noto /configs /customFiles
|
# chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/fonts/opentype/noto /configs /customFiles
|
||||||
|
|
||||||
# Copy necessary files
|
# Copy necessary files
|
||||||
|
COPY ./scripts/download-security-jar.sh /scripts/download-security-jar.sh
|
||||||
|
COPY ./scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
|
||||||
|
COPY ./pipeline/ /pipeline/
|
||||||
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
||||||
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
|
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
|
||||||
COPY build/libs/*.jar app.jar
|
COPY build/libs/*.jar app.jar
|
||||||
|
|
||||||
# Set font cache and permissions
|
# Set font cache and permissions
|
||||||
RUN fc-cache -f -v
|
RUN fc-cache -f -v && \
|
||||||
|
chmod +x /scripts/init-without-ocr.sh && \
|
||||||
|
chmod +x /scripts/download-security-jar.sh
|
||||||
|
|
||||||
|
|
||||||
# chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
# chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||||
|
|
||||||
|
|
||||||
@@ -50,5 +61,5 @@ ENV DOCKER_ENABLE_SECURITY=false
|
|||||||
|
|
||||||
# Run the application
|
# Run the application
|
||||||
#USER stirlingpdfuser
|
#USER stirlingpdfuser
|
||||||
|
ENTRYPOINT ["/scripts/init-without-ocr.sh"]
|
||||||
CMD ["java", "-jar", "/app.jar"]
|
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
||||||
|
|||||||
@@ -1,34 +1,46 @@
|
|||||||
# Build jbig2enc in a separate stage
|
# Build jbig2enc in a separate stage
|
||||||
FROM bellsoft/liberica-openjdk-alpine:17
|
FROM bellsoft/liberica-openjdk-alpine:17
|
||||||
|
|
||||||
|
ARG VERSION_TAG
|
||||||
|
|
||||||
# Set Environment Variables
|
# Set Environment Variables
|
||||||
ENV PUID=1000 \
|
ENV DOCKER_ENABLE_SECURITY=false \
|
||||||
PGID=1000 \
|
|
||||||
UMASK=022 \
|
|
||||||
DOCKER_ENABLE_SECURITY=false \
|
|
||||||
HOME=/home/stirlingpdfuser \
|
HOME=/home/stirlingpdfuser \
|
||||||
VERSION_TAG=$VERSION_TAG
|
VERSION_TAG=$VERSION_TAG \
|
||||||
|
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
|
||||||
|
# PUID=1000 \
|
||||||
|
# PGID=1000 \
|
||||||
|
# UMASK=022 \
|
||||||
|
|
||||||
# Create user and group using Alpine's addgroup and adduser
|
# Create user and group using Alpine's addgroup and adduser
|
||||||
RUN addgroup -g $PGID stirlingpdfgroup && \
|
#RUN addgroup -g $PGID stirlingpdfgroup && \
|
||||||
adduser -u $PUID -G stirlingpdfgroup -s /bin/sh -D stirlingpdfuser && \
|
# adduser -u $PUID -G stirlingpdfgroup -s /bin/sh -D stirlingpdfuser && \
|
||||||
mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
|
# mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
|
||||||
|
|
||||||
# Set up necessary directories and permissions
|
# Set up necessary directories and permissions
|
||||||
RUN mkdir -p /scripts /configs /customFiles && \
|
#RUN mkdir -p /scripts /configs /customFiles && \
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup /scripts /configs /customFiles
|
# chown -R stirlingpdfuser:stirlingpdfgroup /scripts /configs /customFiles /logs /pipeline /pipeline/defaultWebUIConfigs /pipeline/watchedFolders /pipeline/finishedFolders
|
||||||
|
|
||||||
|
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /configs /customFiles
|
||||||
|
COPY ./scripts/download-security-jar.sh /scripts/download-security-jar.sh
|
||||||
|
COPY ./scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
|
||||||
|
COPY ./pipeline/ /pipeline/
|
||||||
COPY build/libs/*.jar app.jar
|
COPY build/libs/*.jar app.jar
|
||||||
|
|
||||||
# Set font cache and permissions
|
# Set font cache and permissions
|
||||||
RUN chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
#RUN chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||||
|
|
||||||
|
RUN chmod +x /scripts/init-without-ocr.sh && \
|
||||||
|
chmod +x /scripts/download-security-jar.sh && \
|
||||||
|
apk add --no-cache curl
|
||||||
|
|
||||||
# Expose the application port
|
# Expose the application port
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
||||||
ENV DOCKER_ENABLE_SECURITY=false
|
|
||||||
|
ENTRYPOINT ["/scripts/init-without-ocr.sh"]
|
||||||
|
|
||||||
# Run the application
|
# Run the application
|
||||||
CMD ["java", "-jar", "/app.jar"]
|
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ RUN apt-get update && \
|
|||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
openjdk-17-jre
|
openjdk-17-jre
|
||||||
|
|
||||||
|
|
||||||
# Doc conversion
|
# Doc conversion
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
libreoffice-core-nogui \
|
libreoffice-core \
|
||||||
libreoffice-common \
|
libreoffice-common \
|
||||||
libreoffice-writer-nogui \
|
libreoffice-writer \
|
||||||
libreoffice-calc-nogui \
|
libreoffice-calc \
|
||||||
libreoffice-impress-nogui \
|
libreoffice-impress \
|
||||||
python3-uno \
|
python3-uno \
|
||||||
|
curl \
|
||||||
unoconv
|
unoconv
|
||||||
|
|
||||||
|
|
||||||
@@ -35,8 +35,8 @@ apt-get update && \
|
|||||||
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
|
#CV and HTML
|
||||||
RUN pip install --no-cache-dir opencv-python-headless
|
RUN pip install --no-cache-dir opencv-python-headless WeasyPrint
|
||||||
|
|
||||||
|
|
||||||
# cleanup and etc
|
# cleanup and etc
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Install the following software, if not already installed:
|
|||||||
|
|
||||||
- Git
|
- Git
|
||||||
|
|
||||||
- Python 3 (with pip)
|
- Python 3.8 (with pip)
|
||||||
|
|
||||||
- Make
|
- Make
|
||||||
|
|
||||||
@@ -95,14 +95,14 @@ For Debian-based systems, you can use the following command:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt-get install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
|
sudo apt-get install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
|
||||||
pip3 install uno opencv-python-headless unoconv pngquant
|
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
|
||||||
```
|
```
|
||||||
|
|
||||||
For Fedora:
|
For Fedora:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
|
sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
|
||||||
pip3 install uno opencv-python-headless unoconv pngquant
|
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 4: Clone and Build Stirling-PDF
|
### Step 4: Clone and Build Stirling-PDF
|
||||||
@@ -176,7 +176,7 @@ rpm -qa | grep tesseract-langpack | sed 's/tesseract-langpack-//g'
|
|||||||
```bash
|
```bash
|
||||||
./gradlew bootRun
|
./gradlew bootRun
|
||||||
or
|
or
|
||||||
java -jar build/libs/app.jar
|
java -jar /opt/Stirling-PDF/Stirling-PDF-*.jar
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 8: Adding a Desktop icon
|
### Step 8: Adding a Desktop icon
|
||||||
@@ -202,6 +202,64 @@ EOF
|
|||||||
|
|
||||||
Note: Currently the app will run in the background until manually closed.
|
Note: Currently the app will run in the background until manually closed.
|
||||||
|
|
||||||
|
### Optional: Run Stirling-PDF as a service
|
||||||
|
|
||||||
|
First create a .env file, where you can store environment variables:
|
||||||
|
```
|
||||||
|
touch /opt/Stirling-PDF/.env
|
||||||
|
```
|
||||||
|
In this file you can add all variables, one variable per line, as stated in the main readme (for example SYSTEM_DEFAULTLOCALE="de-DE").
|
||||||
|
|
||||||
|
Create a new file where we store our service settings and open it with nano editor:
|
||||||
|
```
|
||||||
|
nano /etc/systemd/system/stirlingpdf.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Paste this content, make sure to update the filename of the jar-file. Press Ctrl+S and Ctrl+X to save and exit the nano editor:
|
||||||
|
```
|
||||||
|
[Unit]
|
||||||
|
Description=Stirling-PDF service
|
||||||
|
After=syslog.target network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
SuccessExitStatus=143
|
||||||
|
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
|
||||||
|
Type=simple
|
||||||
|
|
||||||
|
EnvironmentFile=/opt/Stirling-PDF/.env
|
||||||
|
WorkingDirectory=/opt/Stirling-PDF
|
||||||
|
ExecStart=/usr/bin/java -jar Stirling-PDF-0.17.2.jar
|
||||||
|
ExecStop=/bin/kill -15 $MAINPID
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Notify systemd that it has to rebuild its internal service database (you have to run this command every time you make a change in the service file):
|
||||||
|
```
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable the service to tell the service to start it automatically:
|
||||||
|
```
|
||||||
|
sudo systemctl enable stirlingpdf.service
|
||||||
|
```
|
||||||
|
|
||||||
|
See the status of the service:
|
||||||
|
```
|
||||||
|
sudo systemctl status stirlingpdf.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Manually start/stop/restart the service:
|
||||||
|
```
|
||||||
|
sudo systemctl start stirlingpdf.service
|
||||||
|
sudo systemctl stop stirlingpdf.service
|
||||||
|
sudo systemctl restart stirlingpdf.service
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Remember to set the necessary environment variables before running the project if you want to customize the application the list can be seen in the main readme.
|
Remember to set the necessary environment variables before running the project if you want to customize the application the list can be seen in the main readme.
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -14,10 +14,9 @@ This is a powerful locally hosted web based PDF manipulation tool using docker t
|
|||||||
|
|
||||||
Stirling PDF makes no outbound calls for any record keeping or tracking.
|
Stirling PDF makes no outbound calls for any record keeping or tracking.
|
||||||
|
|
||||||
All files and PDFs are either purely client side, in server memory only during the execution of the task or within a temporay file only for execution of the task.
|
All files and PDFs exist either exclusively on the client side, reside in server memory only during task execution, or temporarily reside in a file solely for the execution of the task. Any file downloaded by the user will have been deleted from the server by that point.
|
||||||
Any file which has been downloaded by the user will have already been deleted from the server by that time.
|
|
||||||
|
|
||||||
Feel free to request any features or bug fixes either in github issues or our [Discord](https://discord.gg/Cn8pWhQRxZ)
|
Please feel free to submit feature requests or report bugs either through GitHub issues or on our [Discord](https://discord.gg/Cn8pWhQRxZ)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -115,6 +114,7 @@ docker run -d \
|
|||||||
-p 8080:8080 \
|
-p 8080:8080 \
|
||||||
-v /location/of/trainingData:/usr/share/tesseract-ocr/5/tessdata \
|
-v /location/of/trainingData:/usr/share/tesseract-ocr/5/tessdata \
|
||||||
-v /location/of/extraConfigs:/configs \
|
-v /location/of/extraConfigs:/configs \
|
||||||
|
-v /location/of/logs:/logs \
|
||||||
-e DOCKER_ENABLE_SECURITY=false \
|
-e DOCKER_ENABLE_SECURITY=false \
|
||||||
--name stirling-pdf \
|
--name stirling-pdf \
|
||||||
frooodle/s-pdf:latest
|
frooodle/s-pdf:latest
|
||||||
@@ -136,6 +136,7 @@ services:
|
|||||||
- /location/of/trainingData:/usr/share/tesseract-ocr/5/tessdata #Required for extra OCR languages
|
- /location/of/trainingData:/usr/share/tesseract-ocr/5/tessdata #Required for extra OCR languages
|
||||||
- /location/of/extraConfigs:/configs
|
- /location/of/extraConfigs:/configs
|
||||||
# - /location/of/customFiles:/customFiles/
|
# - /location/of/customFiles:/customFiles/
|
||||||
|
# - /location/of/logs:/logs/
|
||||||
environment:
|
environment:
|
||||||
- DOCKER_ENABLE_SECURITY=false
|
- DOCKER_ENABLE_SECURITY=false
|
||||||
```
|
```
|
||||||
@@ -146,7 +147,7 @@ Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "pod
|
|||||||
Please view https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md
|
Please view https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md
|
||||||
|
|
||||||
## Want to add your own language?
|
## Want to add your own language?
|
||||||
Stirling PDF currently supports 20!
|
Stirling PDF currently supports 21!
|
||||||
- English (English) (en_GB)
|
- English (English) (en_GB)
|
||||||
- English (US) (en_US)
|
- English (US) (en_US)
|
||||||
- Arabic (العربية) (ar_AR)
|
- Arabic (العربية) (ar_AR)
|
||||||
@@ -167,6 +168,8 @@ Stirling PDF currently supports 20!
|
|||||||
- Dutch (Nederlands) (nl_NL)
|
- Dutch (Nederlands) (nl_NL)
|
||||||
- Greek (el_GR)
|
- Greek (el_GR)
|
||||||
- Turkish (Türkçe) (tr_TR)
|
- Turkish (Türkçe) (tr_TR)
|
||||||
|
- Indonesia (Bahasa Indonesia) (id_ID)
|
||||||
|
- Hindi (हिंदी) (hi_IN)
|
||||||
|
|
||||||
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/Frooodle/Stirling-PDF/blob/main/HowToAddNewLanguage.md
|
||||||
@@ -265,7 +268,7 @@ For API usage you must provide a header with 'X-API-Key' and the associated API
|
|||||||
- Fill forms mannual and automatic
|
- Fill forms mannual and automatic
|
||||||
|
|
||||||
### Q2: Why is my application downloading .htm files?
|
### Q2: Why is my application downloading .htm files?
|
||||||
This is a issue caused commonly by your NGINX congifuration. The default file upload size for NGINX is 1MB, you need to add the following in your Nginx sites-available file. ``client_max_body_size SIZE;`` Where "SIZE" is 50M for example for 50MB files.
|
This is an issue caused commonly by your NGINX configuration. The default file upload size for NGINX is 1MB, you need to add the following in your Nginx sites-available file. ``client_max_body_size SIZE;`` Where "SIZE" is 50M for example for 50MB files.
|
||||||
|
|
||||||
### Q3: Why is my download timing out
|
### Q3: Why is my download timing out
|
||||||
NGINX has timeout values by default so if you are running Stirling-PDF behind NGINX you may need to set a timeout value such as adding the config ``proxy_read_timeout 3600;``
|
NGINX has timeout values by default so if you are running Stirling-PDF behind NGINX you may need to set a timeout value such as adding the config ``proxy_read_timeout 3600;``
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ add-image | ✔️ | ✔️ | ✔️
|
|||||||
add-watermark | ✔️ | ✔️ | ✔️
|
add-watermark | ✔️ | ✔️ | ✔️
|
||||||
adjust-contrast | ✔️ | ✔️ | ✔️
|
adjust-contrast | ✔️ | ✔️ | ✔️
|
||||||
auto-split-pdf | ✔️ | ✔️ | ✔️
|
auto-split-pdf | ✔️ | ✔️ | ✔️
|
||||||
|
auto-redact | ✔️ | ✔️ | ✔️
|
||||||
auto-rename | ✔️ | ✔️ | ✔️
|
auto-rename | ✔️ | ✔️ | ✔️
|
||||||
cert-sign | ✔️ | ✔️ | ✔️
|
cert-sign | ✔️ | ✔️ | ✔️
|
||||||
crop | ✔️ | ✔️ | ✔️
|
crop | ✔️ | ✔️ | ✔️
|
||||||
@@ -33,7 +34,9 @@ img-to-pdf | ✔️ | ✔️ | ✔️
|
|||||||
markdown-to-pdf | ✔️ | ✔️ | ✔️
|
markdown-to-pdf | ✔️ | ✔️ | ✔️
|
||||||
merge-pdfs | ✔️ | ✔️ | ✔️
|
merge-pdfs | ✔️ | ✔️ | ✔️
|
||||||
multi-page-layout | ✔️ | ✔️ | ✔️
|
multi-page-layout | ✔️ | ✔️ | ✔️
|
||||||
|
overlay-pdf | ✔️ | ✔️ | ✔️
|
||||||
pdf-organizer | ✔️ | ✔️ | ✔️
|
pdf-organizer | ✔️ | ✔️ | ✔️
|
||||||
|
pdf-to-csv | ✔️ | ✔️ | ✔️
|
||||||
pdf-to-img | ✔️ | ✔️ | ✔️
|
pdf-to-img | ✔️ | ✔️ | ✔️
|
||||||
pdf-to-single-page | ✔️ | ✔️ | ✔️
|
pdf-to-single-page | ✔️ | ✔️ | ✔️
|
||||||
remove-pages | ✔️ | ✔️ | ✔️
|
remove-pages | ✔️ | ✔️ | ✔️
|
||||||
@@ -43,6 +46,8 @@ sanitize-pdf | ✔️ | ✔️ | ✔️
|
|||||||
scale-pages | ✔️ | ✔️ | ✔️
|
scale-pages | ✔️ | ✔️ | ✔️
|
||||||
sign | ✔️ | ✔️ | ✔️
|
sign | ✔️ | ✔️ | ✔️
|
||||||
show-javascript | ✔️ | ✔️ | ✔️
|
show-javascript | ✔️ | ✔️ | ✔️
|
||||||
|
split-by-size-or-count | ✔️ | ✔️ | ✔️
|
||||||
|
split-pdf-by-sections | ✔️ | ✔️ | ✔️
|
||||||
split-pdfs | ✔️ | ✔️ | ✔️
|
split-pdfs | ✔️ | ✔️ | ✔️
|
||||||
file-to-pdf | | ✔️ | ✔️
|
file-to-pdf | | ✔️ | ✔️
|
||||||
pdf-to-html | | ✔️ | ✔️
|
pdf-to-html | | ✔️ | ✔️
|
||||||
|
|||||||
98
build.gradle
98
build.gradle
@@ -1,18 +1,19 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'org.springframework.boot' version '3.1.2'
|
id 'org.springframework.boot' version '3.1.2'
|
||||||
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.6.0'
|
id 'org.springdoc.openapi-gradle-plugin' version '1.8.0'
|
||||||
id "io.swagger.swaggerhub" version "1.2.0"
|
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'
|
||||||
}
|
}
|
||||||
|
|
||||||
group = 'stirling.software'
|
group = 'stirling.software'
|
||||||
version = '0.16.0'
|
version = '0.18.1'
|
||||||
sourceCompatibility = '17'
|
sourceCompatibility = '17'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@@ -32,7 +33,6 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
openApi {
|
openApi {
|
||||||
apiDocsUrl = "http://localhost:8080/v1/api-docs"
|
apiDocsUrl = "http://localhost:8080/v1/api-docs"
|
||||||
outputDir = file("$projectDir")
|
outputDir = file("$projectDir")
|
||||||
@@ -49,7 +49,7 @@ launch4j {
|
|||||||
|
|
||||||
errTitle="Encountered error, Do you have Java 17?"
|
errTitle="Encountered error, Do you have Java 17?"
|
||||||
downloadUrl="https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe"
|
downloadUrl="https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe"
|
||||||
variables=["BROWSER_OPEN=true"]
|
variables=["BROWSER_OPEN=true", "ENDPOINTS_GROUPS_TO_REMOVE=CLI"]
|
||||||
jreMinVersion="17"
|
jreMinVersion="17"
|
||||||
|
|
||||||
mutexName="Stirling-PDF"
|
mutexName="Stirling-PDF"
|
||||||
@@ -62,37 +62,80 @@ 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
|
||||||
|
implementation 'ch.qos.logback:logback-classic:1.4.14'
|
||||||
|
implementation 'ch.qos.logback:logback-core:1.4.14'
|
||||||
|
implementation 'org.springframework:spring-webmvc:6.0.15'
|
||||||
|
|
||||||
implementation 'org.yaml:snakeyaml:2.1'
|
implementation 'org.yaml:snakeyaml:2.1'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web:3.1.2'
|
implementation 'org.springframework.boot:spring-boot-starter-web:3.2.1'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.1.2'
|
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.1.2'
|
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"
|
||||||
implementation "com.h2database:h2"
|
implementation "com.h2database:h2"
|
||||||
}
|
}
|
||||||
|
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.1.4'
|
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.2.1'
|
||||||
|
|
||||||
|
// Batik
|
||||||
|
implementation 'org.apache.xmlgraphics:batik-all:1.17'
|
||||||
|
|
||||||
|
// TwelveMonkeys
|
||||||
|
implementation 'com.twelvemonkeys.imageio:imageio-batik:3.10.1'
|
||||||
|
implementation 'com.twelvemonkeys.imageio:imageio-bmp:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-hdr:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-icns:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-iff:3.10.1'
|
||||||
|
implementation 'com.twelvemonkeys.imageio:imageio-jpeg:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-pcx:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-pict:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-pnm:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-psd:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-sgi:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-tga:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-thumbsdb:3.10.1'
|
||||||
|
implementation 'com.twelvemonkeys.imageio:imageio-tiff:3.10.1'
|
||||||
|
implementation 'com.twelvemonkeys.imageio:imageio-webp:3.10.1'
|
||||||
|
// implementation 'com.twelvemonkeys.imageio:imageio-xwd:3.10.1'
|
||||||
|
|
||||||
// https://mvnrepository.com/artifact/org.apache.pdfbox/jbig2-imageio
|
implementation 'commons-io:commons-io:2.15.1'
|
||||||
implementation group: 'org.apache.pdfbox', name: 'jbig2-imageio', version: '3.0.4'
|
|
||||||
implementation 'com.github.jai-imageio:jai-imageio-core:1.4.0'
|
|
||||||
implementation 'com.github.jai-imageio:jai-imageio-jpeg2000:1.3.0'
|
|
||||||
implementation 'commons-io:commons-io:2.13.0'
|
|
||||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
|
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
|
||||||
|
|
||||||
//general PDF
|
//general PDF
|
||||||
|
|
||||||
// https://mvnrepository.com/artifact/com.opencsv/opencsv
|
// https://mvnrepository.com/artifact/com.opencsv/opencsv
|
||||||
implementation group: 'com.opencsv', name: 'opencsv', version: '5.7.1'
|
implementation ('com.opencsv:opencsv:5.7.1') {
|
||||||
implementation 'org.apache.pdfbox:pdfbox:2.0.29'
|
exclude group: 'commons-logging', module: 'commons-logging'
|
||||||
implementation 'org.apache.pdfbox:xmpbox:2.0.29'
|
}
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
|
|
||||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
|
implementation ('org.apache.pdfbox:pdfbox:2.0.29'){
|
||||||
|
exclude group: 'commons-logging', module: 'commons-logging'
|
||||||
|
}
|
||||||
|
|
||||||
|
implementation ('org.apache.pdfbox:xmpbox:2.0.29'){
|
||||||
|
exclude group: 'commons-logging', module: 'commons-logging'
|
||||||
|
}
|
||||||
|
|
||||||
|
implementation 'org.bouncycastle:bcprov-jdk18on:1.77'
|
||||||
|
implementation 'org.bouncycastle:bcpkix-jdk18on:1.77'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||||
implementation 'io.micrometer:micrometer-core'
|
implementation 'io.micrometer:micrometer-core'
|
||||||
implementation group: 'com.google.zxing', name: 'core', version: '3.5.2'
|
implementation group: 'com.google.zxing', name: 'core', version: '3.5.2'
|
||||||
@@ -102,10 +145,13 @@ dependencies {
|
|||||||
implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0'
|
implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0'
|
||||||
|
|
||||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||||
compileOnly 'org.projectlombok:lombok:1.18.28'
|
compileOnly 'org.projectlombok:lombok:1.18.30'
|
||||||
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')
|
||||||
@@ -136,7 +182,7 @@ jar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('test') {
|
tasks.named('test') {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
task printVersion {
|
task printVersion {
|
||||||
|
|||||||
39
pipeline/defaultWebUIConfigs/Prepare-pdfs-for-email.json
Normal file
39
pipeline/defaultWebUIConfigs/Prepare-pdfs-for-email.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "Prepare-pdfs-for-email",
|
||||||
|
"pipeline": [
|
||||||
|
{
|
||||||
|
"operation": "/api/v1/misc/repair",
|
||||||
|
"parameters": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation": "/api/v1/security/sanitize-pdf",
|
||||||
|
"parameters": {
|
||||||
|
"removeJavaScript": true,
|
||||||
|
"removeEmbeddedFiles": false,
|
||||||
|
"removeMetadata": false,
|
||||||
|
"removeLinks": false,
|
||||||
|
"removeFonts": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation": "/api/v1/misc/compress-pdf",
|
||||||
|
"parameters": {
|
||||||
|
"optimizeLevel": 2,
|
||||||
|
"expectedOutputSize": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation": "/api/v1/general/split-by-size-or-count",
|
||||||
|
"parameters": {
|
||||||
|
"splitType": 0,
|
||||||
|
"splitValue": "15MB"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"_examples": {
|
||||||
|
"outputDir": "{outputFolder}/{folderName}",
|
||||||
|
"outputFileName": "{filename}-{pipelineName}-{date}-{time}"
|
||||||
|
},
|
||||||
|
"outputDir": "httpWebRequest",
|
||||||
|
"outputFileName": "{filename}"
|
||||||
|
}
|
||||||
33
pipeline/defaultWebUIConfigs/split-rotate-auto-rename.json
Normal file
33
pipeline/defaultWebUIConfigs/split-rotate-auto-rename.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "split-rotate-auto-rename",
|
||||||
|
"pipeline": [
|
||||||
|
{
|
||||||
|
"operation": "/api/v1/general/split-pdf-by-sections",
|
||||||
|
"parameters": {
|
||||||
|
"horizontalDivisions": 2,
|
||||||
|
"verticalDivisions": 2,
|
||||||
|
"fileInput": "automated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation": "/api/v1/general/rotate-pdf",
|
||||||
|
"parameters": {
|
||||||
|
"angle": 90,
|
||||||
|
"fileInput": "automated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation": "/api/v1/misc/auto-rename",
|
||||||
|
"parameters": {
|
||||||
|
"useFirstTextAsFallback": false,
|
||||||
|
"fileInput": "automated"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"_examples": {
|
||||||
|
"outputDir": "{outputFolder}/{folderName}",
|
||||||
|
"outputFileName": "{filename}-{pipelineName}-{date}-{time}"
|
||||||
|
},
|
||||||
|
"outputDir": "{outputFolder}",
|
||||||
|
"outputFileName": "{filename}"
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import cv2
|
import cv2
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
def is_blank_image(image_path, threshold=10, white_percent=99, white_value=255, blur_size=5):
|
def is_blank_image(image_path, threshold=10, white_percent=99, white_value=255, blur_size=5):
|
||||||
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
|
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
|
||||||
|
|
||||||
if image is None:
|
if image is None:
|
||||||
print(f"Error: Unable to read the image file: {image_path}")
|
print(f"Error: Unable to read the image file: {image_path}")
|
||||||
return False
|
return False
|
||||||
@@ -15,19 +16,11 @@ def is_blank_image(image_path, threshold=10, white_percent=99, white_value=255,
|
|||||||
_, thresholded_image = cv2.threshold(blurred_image, white_value - threshold, white_value, cv2.THRESH_BINARY)
|
_, thresholded_image = cv2.threshold(blurred_image, white_value - threshold, white_value, cv2.THRESH_BINARY)
|
||||||
|
|
||||||
# Calculate the percentage of white pixels in the thresholded image
|
# Calculate the percentage of white pixels in the thresholded image
|
||||||
white_pixels = 0
|
white_pixels = np.sum(thresholded_image == white_value)
|
||||||
total_pixels = thresholded_image.size
|
white_pixel_percentage = (white_pixels / thresholded_image.size) * 100
|
||||||
for i in range(0, thresholded_image.shape[0], 2):
|
|
||||||
for j in range(0, thresholded_image.shape[1], 2):
|
|
||||||
if thresholded_image[i, j] == white_value:
|
|
||||||
white_pixels += 1
|
|
||||||
white_pixel_percentage = (white_pixels / (i * thresholded_image.shape[1] + j + 1)) * 100
|
|
||||||
if white_pixel_percentage < white_percent:
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f"Page has white pixel percent of {white_pixel_percentage}")
|
print(f"Page has white pixel percent of {white_pixel_percentage}")
|
||||||
return True
|
return white_pixel_percentage >= white_percent
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -39,9 +32,6 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
blank = is_blank_image(args.image_path, args.threshold, args.white_percent)
|
blank = is_blank_image(args.image_path, args.threshold, args.white_percent)
|
||||||
|
|
||||||
if blank:
|
# Return code 1: The image is considered blank.
|
||||||
# Return code 1: The image is considered blank.
|
# Return code 0: The image is not considered blank.
|
||||||
sys.exit(1)
|
sys.exit(int(blank))
|
||||||
else:
|
|
||||||
# Return code 0: The image is not considered blank.
|
|
||||||
sys.exit(0)
|
|
||||||
|
|||||||
19
scripts/download-security-jar.sh
Normal file
19
scripts/download-security-jar.sh
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
echo "Running Stirling PDF with DOCKER_ENABLE_SECURITY=${DOCKER_ENABLE_SECURITY} and VERSION_TAG=${VERSION_TAG}"
|
||||||
|
# Check for DOCKER_ENABLE_SECURITY and download the appropriate JAR if required
|
||||||
|
if [ "$DOCKER_ENABLE_SECURITY" = "true" ] && [ "$VERSION_TAG" != "alpha" ]; 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"
|
||||||
|
curl -L -o app-security.jar https://github.com/Frooodle/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar
|
||||||
|
|
||||||
|
# If the first download attempt failed, try with the 'v' prefix
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Trying to download from: https://github.com/Frooodle/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
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then # checks if curl was successful
|
||||||
|
rm -f app.jar
|
||||||
|
ln -s app-security.jar app.jar
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
6
scripts/init-without-ocr.sh
Normal file
6
scripts/init-without-ocr.sh
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
/scripts/download-security-jar.sh
|
||||||
|
|
||||||
|
# Run the main command
|
||||||
|
exec "$@"
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
# Copy the original tesseract-ocr files to the volume directory without overwriting existing files
|
# Copy the original tesseract-ocr files to the volume directory without overwriting existing files
|
||||||
echo "Copying original files without overwriting existing files"
|
echo "Copying original files without overwriting existing files"
|
||||||
mkdir -p /usr/share/tesseract-ocr
|
mkdir -p /usr/share/tesseract-ocr
|
||||||
cp -rn /usr/share/tesseract-ocr-original/* /usr/share/tesseract-ocr
|
cp -rn /usr/share/tesseract-ocr-original/* /usr/share/tesseract-ocr
|
||||||
|
|
||||||
if [ -d /usr/share/tesseract-ocr/4.00/tessdata ]; then
|
if [ -d /usr/share/tesseract-ocr/4.00/tessdata ]; then
|
||||||
cp -r /usr/share/tesseract-ocr/4.00/tessdata/* /usr/share/tesseract-ocr/5/tessdata/ || true;
|
cp -r /usr/share/tesseract-ocr/4.00/tessdata/* /usr/share/tesseract-ocr/5/tessdata/ || true;
|
||||||
@@ -20,25 +20,7 @@ if [[ -n "$TESSERACT_LANGS" ]]; then
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check for DOCKER_ENABLE_SECURITY and download the appropriate JAR if required
|
/scripts/download-security-jar.sh
|
||||||
if [ "$DOCKER_ENABLE_SECURITY" = "true" ] && [ "$VERSION_TAG" != "alpha" ]; 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"
|
|
||||||
curl -L -o app-security.jar https://github.com/Frooodle/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar
|
|
||||||
|
|
||||||
# If the first download attempt failed, try with the 'v' prefix
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "Trying to download from: https://github.com/Frooodle/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
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then # checks if curl was successful
|
|
||||||
rm -f app.jar
|
|
||||||
ln -s app-security.jar app.jar
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
# Run the main command
|
# Run the main command
|
||||||
exec "$@"
|
exec "$@"
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ 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 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
|
|
||||||
|
|
||||||
//@EnableScheduling
|
@SpringBootApplication
|
||||||
|
@EnableScheduling
|
||||||
public class SPdfApplication {
|
public class SPdfApplication {
|
||||||
|
|
||||||
@Autowired
|
@Autowired private Environment env;
|
||||||
private Environment env;
|
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
@@ -28,11 +28,7 @@ public class SPdfApplication {
|
|||||||
|
|
||||||
if (browserOpen) {
|
if (browserOpen) {
|
||||||
try {
|
try {
|
||||||
String port = env.getProperty("local.server.port");
|
String url = "http://localhost:" + getPort();
|
||||||
if(port == null || port.length() == 0) {
|
|
||||||
port="8080";
|
|
||||||
}
|
|
||||||
String url = "http://localhost:" + port;
|
|
||||||
|
|
||||||
String os = System.getProperty("os.name").toLowerCase();
|
String os = System.getProperty("os.name").toLowerCase();
|
||||||
Runtime rt = Runtime.getRuntime();
|
Runtime rt = Runtime.getRuntime();
|
||||||
@@ -45,38 +41,41 @@ public class SPdfApplication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(Collections.singletonMap("spring.config.additional-location", "file:configs/settings.yml"));
|
app.setDefaultProperties(
|
||||||
|
Collections.singletonMap(
|
||||||
|
"spring.config.additional-location", "file:configs/settings.yml"));
|
||||||
} else {
|
} else {
|
||||||
System.out.println("External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
|
System.out.println(
|
||||||
|
"External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
|
||||||
}
|
}
|
||||||
app.run(args);
|
app.run(args);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Thread.sleep(1000);
|
Thread.sleep(1000);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
// TODO Auto-generated catch block
|
// TODO Auto-generated catch block
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
GeneralUtils.createDir("customFiles/static/");
|
GeneralUtils.createDir("customFiles/static/");
|
||||||
GeneralUtils.createDir("customFiles/templates/");
|
GeneralUtils.createDir("customFiles/templates/");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
System.out.println("Stirling-PDF Started.");
|
System.out.println("Stirling-PDF Started.");
|
||||||
|
|
||||||
String port = System.getProperty("local.server.port");
|
String url = "http://localhost:" + getPort();
|
||||||
if(port == null || port.length() == 0) {
|
|
||||||
port="8080";
|
|
||||||
}
|
|
||||||
String url = "http://localhost:" + port;
|
|
||||||
System.out.println("Navigate to " + url);
|
System.out.println("Navigate to " + url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getPort() {
|
||||||
}
|
String port = System.getProperty("local.server.port");
|
||||||
|
if (port == null || port.isEmpty()) {
|
||||||
|
port = "8080";
|
||||||
|
}
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import org.springframework.beans.PropertyEditorRegistrar;
|
|
||||||
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 stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class AppConfig {
|
public class AppConfig {
|
||||||
|
|
||||||
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
@Autowired
|
|
||||||
ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
@Bean(name = "loginEnabled")
|
@Bean(name = "loginEnabled")
|
||||||
public boolean loginEnabled() {
|
public boolean loginEnabled() {
|
||||||
return applicationProperties.getSecurity().getEnableLogin();
|
return applicationProperties.getSecurity().getEnableLogin();
|
||||||
@@ -20,7 +18,7 @@ public class AppConfig {
|
|||||||
|
|
||||||
@Bean(name = "appName")
|
@Bean(name = "appName")
|
||||||
public String appName() {
|
public String appName() {
|
||||||
String homeTitle = applicationProperties.getUi().getAppName();
|
String homeTitle = applicationProperties.getUi().getAppName();
|
||||||
return (homeTitle != null) ? homeTitle : "Stirling PDF";
|
return (homeTitle != null) ? homeTitle : "Stirling PDF";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,24 +30,31 @@ public class AppConfig {
|
|||||||
|
|
||||||
@Bean(name = "homeText")
|
@Bean(name = "homeText")
|
||||||
public String homeText() {
|
public String homeText() {
|
||||||
return (applicationProperties.getUi().getHomeDescription() != null) ? applicationProperties.getUi().getHomeDescription() : "null";
|
return (applicationProperties.getUi().getHomeDescription() != null)
|
||||||
|
? applicationProperties.getUi().getHomeDescription()
|
||||||
|
: "null";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Bean(name = "navBarText")
|
@Bean(name = "navBarText")
|
||||||
public String navBarText() {
|
public String navBarText() {
|
||||||
String defaultNavBar = applicationProperties.getUi().getAppNameNavbar() != null ? applicationProperties.getUi().getAppNameNavbar() : applicationProperties.getUi().getAppName();
|
String defaultNavBar =
|
||||||
|
applicationProperties.getUi().getAppNameNavbar() != null
|
||||||
|
? applicationProperties.getUi().getAppNameNavbar()
|
||||||
|
: applicationProperties.getUi().getAppName();
|
||||||
return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF";
|
return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "rateLimit")
|
@Bean(name = "enableAlphaFunctionality")
|
||||||
|
public boolean enableAlphaFunctionality() {
|
||||||
|
return applicationProperties.getSystem().getEnableAlphaFunctionality() != null
|
||||||
|
? applicationProperties.getSystem().getEnableAlphaFunctionality()
|
||||||
|
: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean(name = "rateLimit")
|
||||||
public boolean rateLimit() {
|
public boolean rateLimit() {
|
||||||
String appName = System.getProperty("rateLimit");
|
String appName = System.getProperty("rateLimit");
|
||||||
if (appName == null)
|
if (appName == null) appName = System.getenv("rateLimit");
|
||||||
appName = System.getenv("rateLimit");
|
|
||||||
System.out.println("rateLimit=" + appName);
|
|
||||||
return (appName != null) ? Boolean.valueOf(appName) : false;
|
return (appName != null) ? Boolean.valueOf(appName) : false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,10 +15,9 @@ 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());
|
||||||
@@ -35,25 +34,26 @@ public class Beans implements WebMvcConfigurer {
|
|||||||
@Bean
|
@Bean
|
||||||
public LocaleResolver localeResolver() {
|
public LocaleResolver localeResolver() {
|
||||||
SessionLocaleResolver slr = new SessionLocaleResolver();
|
SessionLocaleResolver slr = new SessionLocaleResolver();
|
||||||
|
|
||||||
|
|
||||||
String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale();
|
String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale();
|
||||||
Locale defaultLocale = 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("Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK.");
|
System.err.println(
|
||||||
|
"Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,5 +61,4 @@ public class Beans implements WebMvcConfigurer {
|
|||||||
slr.setDefaultLocale(defaultLocale);
|
slr.setDefaultLocale(defaultLocale);
|
||||||
return slr;
|
return slr;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,56 +13,62 @@ 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
|
||||||
@Override
|
public boolean preHandle(
|
||||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
|
HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
String queryString = request.getQueryString();
|
String queryString = request.getQueryString();
|
||||||
if (queryString != null && !queryString.isEmpty()) {
|
if (queryString != null && !queryString.isEmpty()) {
|
||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
Map<String, String> parameters = new HashMap<>();
|
Map<String, String> parameters = new HashMap<>();
|
||||||
|
|
||||||
// Keep only the allowed parameters
|
// Keep only the allowed parameters
|
||||||
String[] queryParameters = queryString.split("&");
|
String[] queryParameters = queryString.split("&");
|
||||||
for (String param : queryParameters) {
|
for (String param : queryParameters) {
|
||||||
String[] keyValue = param.split("=");
|
String[] keyValue = param.split("=");
|
||||||
if (keyValue.length != 2) {
|
if (keyValue.length != 2) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (ALLOWED_PARAMS.contains(keyValue[0])) {
|
if (ALLOWED_PARAMS.contains(keyValue[0])) {
|
||||||
parameters.put(keyValue[0], keyValue[1]);
|
parameters.put(keyValue[0], keyValue[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are any parameters that are not allowed
|
// If there are any parameters that are not allowed
|
||||||
if (parameters.size() != queryParameters.length) {
|
if (parameters.size() != queryParameters.length) {
|
||||||
// Construct new query string
|
// Construct new query string
|
||||||
StringBuilder newQueryString = new StringBuilder();
|
StringBuilder newQueryString = new StringBuilder();
|
||||||
for (Map.Entry<String, String> entry : parameters.entrySet()) {
|
for (Map.Entry<String, String> entry : parameters.entrySet()) {
|
||||||
if (newQueryString.length() > 0) {
|
if (newQueryString.length() > 0) {
|
||||||
newQueryString.append("&");
|
newQueryString.append("&");
|
||||||
}
|
}
|
||||||
newQueryString.append(entry.getKey()).append("=").append(entry.getValue());
|
newQueryString.append(entry.getKey()).append("=").append(entry.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to the URL with only allowed query parameters
|
// Redirect to the URL with only allowed query parameters
|
||||||
String redirectUrl = requestURI + "?" + newQueryString;
|
String redirectUrl = requestURI + "?" + newQueryString;
|
||||||
response.sendRedirect(redirectUrl);
|
response.sendRedirect(redirectUrl);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
|
public void postHandle(
|
||||||
ModelAndView modelAndView) {
|
HttpServletRequest request,
|
||||||
}
|
HttpServletResponse response,
|
||||||
|
Object handler,
|
||||||
|
ModelAndView modelAndView) {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
|
public void afterCompletion(
|
||||||
Exception ex) {
|
HttpServletRequest request,
|
||||||
}
|
HttpServletResponse response,
|
||||||
|
Object handler,
|
||||||
|
Exception ex) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class EndpointConfiguration {
|
public class EndpointConfiguration {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
|
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
|
||||||
@@ -26,16 +27,16 @@ public class EndpointConfiguration {
|
|||||||
init();
|
init();
|
||||||
processEnvironmentConfigs();
|
processEnvironmentConfigs();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void enableEndpoint(String endpoint) {
|
public void enableEndpoint(String endpoint) {
|
||||||
endpointStatuses.put(endpoint, true);
|
endpointStatuses.put(endpoint, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void disableEndpoint(String endpoint) {
|
public void disableEndpoint(String endpoint) {
|
||||||
if(!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
|
if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
|
||||||
logger.info("Disabling {}", endpoint);
|
logger.info("Disabling {}", endpoint);
|
||||||
endpointStatuses.put(endpoint, false);
|
endpointStatuses.put(endpoint, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEndpointEnabled(String endpoint) {
|
public boolean isEndpointEnabled(String endpoint) {
|
||||||
@@ -66,7 +67,7 @@ public class EndpointConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void init() {
|
public void init() {
|
||||||
// Adding endpoints to "PageOps" group
|
// Adding endpoints to "PageOps" group
|
||||||
addEndpointToGroup("PageOps", "remove-pages");
|
addEndpointToGroup("PageOps", "remove-pages");
|
||||||
@@ -81,7 +82,10 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("PageOps", "auto-split-pdf");
|
addEndpointToGroup("PageOps", "auto-split-pdf");
|
||||||
addEndpointToGroup("PageOps", "extract-page");
|
addEndpointToGroup("PageOps", "extract-page");
|
||||||
addEndpointToGroup("PageOps", "pdf-to-single-page");
|
addEndpointToGroup("PageOps", "pdf-to-single-page");
|
||||||
|
addEndpointToGroup("PageOps", "split-by-size-or-count");
|
||||||
|
addEndpointToGroup("PageOps", "overlay-pdf");
|
||||||
|
addEndpointToGroup("PageOps", "split-pdf-by-sections");
|
||||||
|
|
||||||
// Adding endpoints to "Convert" group
|
// Adding endpoints to "Convert" group
|
||||||
addEndpointToGroup("Convert", "pdf-to-img");
|
addEndpointToGroup("Convert", "pdf-to-img");
|
||||||
addEndpointToGroup("Convert", "img-to-pdf");
|
addEndpointToGroup("Convert", "img-to-pdf");
|
||||||
@@ -96,7 +100,8 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Convert", "html-to-pdf");
|
addEndpointToGroup("Convert", "html-to-pdf");
|
||||||
addEndpointToGroup("Convert", "url-to-pdf");
|
addEndpointToGroup("Convert", "url-to-pdf");
|
||||||
addEndpointToGroup("Convert", "markdown-to-pdf");
|
addEndpointToGroup("Convert", "markdown-to-pdf");
|
||||||
|
addEndpointToGroup("Convert", "pdf-to-csv");
|
||||||
|
|
||||||
// Adding endpoints to "Security" group
|
// Adding endpoints to "Security" group
|
||||||
addEndpointToGroup("Security", "add-password");
|
addEndpointToGroup("Security", "add-password");
|
||||||
addEndpointToGroup("Security", "remove-password");
|
addEndpointToGroup("Security", "remove-password");
|
||||||
@@ -104,8 +109,8 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Security", "add-watermark");
|
addEndpointToGroup("Security", "add-watermark");
|
||||||
addEndpointToGroup("Security", "cert-sign");
|
addEndpointToGroup("Security", "cert-sign");
|
||||||
addEndpointToGroup("Security", "sanitize-pdf");
|
addEndpointToGroup("Security", "sanitize-pdf");
|
||||||
|
addEndpointToGroup("Security", "auto-redact");
|
||||||
|
|
||||||
// Adding endpoints to "Other" group
|
// Adding endpoints to "Other" group
|
||||||
addEndpointToGroup("Other", "ocr-pdf");
|
addEndpointToGroup("Other", "ocr-pdf");
|
||||||
addEndpointToGroup("Other", "add-image");
|
addEndpointToGroup("Other", "add-image");
|
||||||
@@ -117,15 +122,14 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Other", "flatten");
|
addEndpointToGroup("Other", "flatten");
|
||||||
addEndpointToGroup("Other", "repair");
|
addEndpointToGroup("Other", "repair");
|
||||||
addEndpointToGroup("Other", "remove-blanks");
|
addEndpointToGroup("Other", "remove-blanks");
|
||||||
|
addEndpointToGroup("Other", "remove-annotations");
|
||||||
addEndpointToGroup("Other", "compare");
|
addEndpointToGroup("Other", "compare");
|
||||||
addEndpointToGroup("Other", "add-page-numbers");
|
addEndpointToGroup("Other", "add-page-numbers");
|
||||||
addEndpointToGroup("Other", "auto-rename");
|
addEndpointToGroup("Other", "auto-rename");
|
||||||
addEndpointToGroup("Other", "get-info-on-pdf");
|
addEndpointToGroup("Other", "get-info-on-pdf");
|
||||||
addEndpointToGroup("Other", "show-javascript");
|
addEndpointToGroup("Other", "show-javascript");
|
||||||
|
|
||||||
|
// CLI
|
||||||
|
|
||||||
//CLI
|
|
||||||
addEndpointToGroup("CLI", "compress-pdf");
|
addEndpointToGroup("CLI", "compress-pdf");
|
||||||
addEndpointToGroup("CLI", "extract-image-scans");
|
addEndpointToGroup("CLI", "extract-image-scans");
|
||||||
addEndpointToGroup("CLI", "remove-blanks");
|
addEndpointToGroup("CLI", "remove-blanks");
|
||||||
@@ -141,19 +145,18 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("CLI", "ocr-pdf");
|
addEndpointToGroup("CLI", "ocr-pdf");
|
||||||
addEndpointToGroup("CLI", "html-to-pdf");
|
addEndpointToGroup("CLI", "html-to-pdf");
|
||||||
addEndpointToGroup("CLI", "url-to-pdf");
|
addEndpointToGroup("CLI", "url-to-pdf");
|
||||||
|
|
||||||
|
// python
|
||||||
//python
|
|
||||||
addEndpointToGroup("Python", "extract-image-scans");
|
addEndpointToGroup("Python", "extract-image-scans");
|
||||||
addEndpointToGroup("Python", "remove-blanks");
|
addEndpointToGroup("Python", "remove-blanks");
|
||||||
addEndpointToGroup("Python", "html-to-pdf");
|
addEndpointToGroup("Python", "html-to-pdf");
|
||||||
addEndpointToGroup("Python", "url-to-pdf");
|
addEndpointToGroup("Python", "url-to-pdf");
|
||||||
|
|
||||||
//openCV
|
// openCV
|
||||||
addEndpointToGroup("OpenCV", "extract-image-scans");
|
addEndpointToGroup("OpenCV", "extract-image-scans");
|
||||||
addEndpointToGroup("OpenCV", "remove-blanks");
|
addEndpointToGroup("OpenCV", "remove-blanks");
|
||||||
|
|
||||||
//LibreOffice
|
// LibreOffice
|
||||||
addEndpointToGroup("LibreOffice", "repair");
|
addEndpointToGroup("LibreOffice", "repair");
|
||||||
addEndpointToGroup("LibreOffice", "file-to-pdf");
|
addEndpointToGroup("LibreOffice", "file-to-pdf");
|
||||||
addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
|
addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
|
||||||
@@ -162,14 +165,13 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("LibreOffice", "pdf-to-text");
|
addEndpointToGroup("LibreOffice", "pdf-to-text");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-html");
|
addEndpointToGroup("LibreOffice", "pdf-to-html");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-xml");
|
addEndpointToGroup("LibreOffice", "pdf-to-xml");
|
||||||
|
|
||||||
|
// OCRmyPDF
|
||||||
//OCRmyPDF
|
|
||||||
addEndpointToGroup("OCRmyPDF", "compress-pdf");
|
addEndpointToGroup("OCRmyPDF", "compress-pdf");
|
||||||
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
|
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
|
||||||
addEndpointToGroup("OCRmyPDF", "ocr-pdf");
|
addEndpointToGroup("OCRmyPDF", "ocr-pdf");
|
||||||
|
|
||||||
//Java
|
// Java
|
||||||
addEndpointToGroup("Java", "merge-pdfs");
|
addEndpointToGroup("Java", "merge-pdfs");
|
||||||
addEndpointToGroup("Java", "remove-pages");
|
addEndpointToGroup("Java", "remove-pages");
|
||||||
addEndpointToGroup("Java", "split-pdfs");
|
addEndpointToGroup("Java", "split-pdfs");
|
||||||
@@ -197,16 +199,19 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Java", "pdf-to-single-page");
|
addEndpointToGroup("Java", "pdf-to-single-page");
|
||||||
addEndpointToGroup("Java", "markdown-to-pdf");
|
addEndpointToGroup("Java", "markdown-to-pdf");
|
||||||
addEndpointToGroup("Java", "show-javascript");
|
addEndpointToGroup("Java", "show-javascript");
|
||||||
|
addEndpointToGroup("Java", "auto-redact");
|
||||||
//Javascript
|
addEndpointToGroup("Java", "pdf-to-csv");
|
||||||
|
addEndpointToGroup("Java", "split-by-size-or-count");
|
||||||
|
addEndpointToGroup("Java", "overlay-pdf");
|
||||||
|
addEndpointToGroup("Java", "split-pdf-by-sections");
|
||||||
|
|
||||||
|
// Javascript
|
||||||
addEndpointToGroup("Javascript", "pdf-organizer");
|
addEndpointToGroup("Javascript", "pdf-organizer");
|
||||||
addEndpointToGroup("Javascript", "sign");
|
addEndpointToGroup("Javascript", "sign");
|
||||||
addEndpointToGroup("Javascript", "compare");
|
addEndpointToGroup("Javascript", "compare");
|
||||||
addEndpointToGroup("Javascript", "adjust-contrast");
|
addEndpointToGroup("Javascript", "adjust-contrast");
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processEnvironmentConfigs() {
|
private void processEnvironmentConfigs() {
|
||||||
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
|
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
|
||||||
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
|
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
|
||||||
@@ -223,6 +228,4 @@ public class EndpointConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ 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(HttpServletRequest request, HttpServletResponse response, Object handler)
|
public boolean preHandle(
|
||||||
|
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)) {
|
||||||
@@ -23,4 +23,4 @@ public class EndpointInterceptor implements HandlerInterceptor {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
@@ -21,4 +22,4 @@ public class MetricsConfig {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
@@ -24,25 +25,39 @@ public class MetricsFilter extends OncePerRequestFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
protected void doFilterInternal(
|
||||||
|
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
String uri = request.getRequestURI();
|
String uri = request.getRequestURI();
|
||||||
|
|
||||||
//System.out.println("uri="+uri + ", method=" + request.getMethod() );
|
// System.out.println("uri="+uri + ", method=" + request.getMethod() );
|
||||||
// Ignore static resources
|
// Ignore static resources
|
||||||
if (!(uri.startsWith("/js") || uri.startsWith("api-docs") || uri.endsWith("robots.txt") || uri.startsWith("/images") || uri.endsWith(".png") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".svg")|| uri.endsWith(".js") || uri.contains("swagger") || uri.startsWith("/api"))) {
|
if (!(uri.startsWith("/js")
|
||||||
Counter counter = Counter.builder("http.requests")
|
|| uri.startsWith("/v1/api-docs")
|
||||||
.tag("uri", uri)
|
|| uri.endsWith("robots.txt")
|
||||||
.tag("method", request.getMethod())
|
|| uri.startsWith("/images")
|
||||||
.register(meterRegistry);
|
|| uri.startsWith("/images")
|
||||||
|
|| uri.endsWith(".png")
|
||||||
|
|| uri.endsWith(".ico")
|
||||||
|
|| uri.endsWith(".css")
|
||||||
|
|| uri.endsWith(".map")
|
||||||
|
|| uri.endsWith(".svg")
|
||||||
|
|| uri.endsWith(".js")
|
||||||
|
|| uri.contains("swagger")
|
||||||
|
|| 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();
|
counter.increment();
|
||||||
//System.out.println("Counted");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,53 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
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.SecurityScheme;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class OpenApiConfig {
|
public class OpenApiConfig {
|
||||||
|
|
||||||
@Bean
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
public OpenAPI customOpenAPI() {
|
|
||||||
String version = getClass().getPackage().getImplementationVersion();
|
|
||||||
if (version == null) {
|
|
||||||
|
|
||||||
version = "1.0.0"; // default version if all else fails
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return new OpenAPI().components(new Components()).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."));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OpenAPI customOpenAPI() {
|
||||||
|
String version = getClass().getPackage().getImplementationVersion();
|
||||||
|
if (version == null) {
|
||||||
|
version = "1.0.0"; // default version if all else fails
|
||||||
|
}
|
||||||
|
|
||||||
|
SecurityScheme apiKeyScheme =
|
||||||
|
new SecurityScheme()
|
||||||
|
.type(SecurityScheme.Type.APIKEY)
|
||||||
|
.in(SecurityScheme.In.HEADER)
|
||||||
|
.name("X-API-KEY");
|
||||||
|
if (!applicationProperties.getSecurity().getEnableLogin()) {
|
||||||
|
return new OpenAPI()
|
||||||
|
.components(new Components())
|
||||||
|
.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."));
|
||||||
|
} 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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;
|
||||||
@@ -17,4 +16,3 @@ public class StartupApplicationListener implements ApplicationListener<ContextRe
|
|||||||
startTime = LocalDateTime.now();
|
startTime = LocalDateTime.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,19 +9,18 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,25 +2,48 @@ package stirling.software.SPDF.config.security;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
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 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 {
|
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||||
|
|
||||||
|
@Autowired private final LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) {
|
||||||
|
this.loginAttemptService = loginAttemptService;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
|
public void onAuthenticationFailure(
|
||||||
throws IOException, ServletException {
|
HttpServletRequest request,
|
||||||
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) {
|
HttpServletResponse response,
|
||||||
setDefaultFailureUrl("/login?error=badcredentials");
|
AuthenticationException exception)
|
||||||
} else if (exception.getClass().isAssignableFrom(LockedException.class)) {
|
throws IOException, ServletException {
|
||||||
setDefaultFailureUrl("/login?error=locked");
|
String ip = request.getRemoteAddr();
|
||||||
|
logger.error("Failed login attempt from IP: " + ip);
|
||||||
|
|
||||||
|
String username = request.getParameter("username");
|
||||||
|
if (loginAttemptService.loginAttemptCheck(username)) {
|
||||||
|
setDefaultFailureUrl("/login?error=locked");
|
||||||
|
|
||||||
|
} else {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||||
|
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class CustomAuthenticationSuccessHandler
|
||||||
|
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||||
|
|
||||||
|
@Autowired private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAuthenticationSuccess(
|
||||||
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
String username = request.getParameter("username");
|
||||||
|
loginAttemptService.loginSucceeded(username);
|
||||||
|
|
||||||
|
// Get the saved request
|
||||||
|
HttpSession session = request.getSession(false);
|
||||||
|
SavedRequest savedRequest =
|
||||||
|
session != null
|
||||||
|
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
|
||||||
|
: null;
|
||||||
|
if (savedRequest != null
|
||||||
|
&& !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) {
|
||||||
|
// Redirect to the original destination
|
||||||
|
super.onAuthenticationSuccess(request, response, authentication);
|
||||||
|
} else {
|
||||||
|
// Redirect to the root URL (considering context path)
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
// super.onAuthenticationSuccess(request, response, authentication);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ 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.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;
|
||||||
@@ -19,27 +20,38 @@ 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;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
User user = userRepository.findByUsername(username)
|
User user =
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + username));
|
userRepository
|
||||||
|
.findByUsername(username)
|
||||||
|
.orElseThrow(
|
||||||
|
() ->
|
||||||
|
new UsernameNotFoundException(
|
||||||
|
"No user found with username: " + username));
|
||||||
|
|
||||||
|
if (loginAttemptService.isBlocked(username)) {
|
||||||
|
throw new LockedException(
|
||||||
|
"Your account has been locked due to too many failed login attempts.");
|
||||||
|
}
|
||||||
|
|
||||||
return new org.springframework.security.core.userdetails.User(
|
return new org.springframework.security.core.userdetails.User(
|
||||||
user.getUsername(),
|
user.getUsername(),
|
||||||
user.getPassword(),
|
user.getPassword(),
|
||||||
user.isEnabled(),
|
user.isEnabled(),
|
||||||
true, true, true,
|
true,
|
||||||
getAuthorities(user.getAuthorities())
|
true,
|
||||||
);
|
true,
|
||||||
|
getAuthorities(user.getAuthorities()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) {
|
private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) {
|
||||||
return authorities.stream()
|
return authorities.stream()
|
||||||
.map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
|
.map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,35 +15,35 @@ 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.User;
|
import stirling.software.SPDF.model.User;
|
||||||
|
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();
|
||||||
boolean isStaticResource = requestURI.startsWith("/css/")
|
String requestURI = request.getRequestURI();
|
||||||
|| requestURI.startsWith("/js/")
|
// Check if the request is for static resources
|
||||||
|| requestURI.startsWith("/images/")
|
boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI);
|
||||||
|| requestURI.startsWith("/public/")
|
|
||||||
|| requestURI.endsWith(".svg");
|
|
||||||
|
|
||||||
// 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) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import jakarta.servlet.Filter;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.ServletRequest;
|
||||||
|
import jakarta.servlet.ServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
|
public class IPRateLimitingFilter implements Filter {
|
||||||
|
|
||||||
|
private final ConcurrentHashMap<String, AtomicInteger> requestCounts =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
private final ConcurrentHashMap<String, AtomicInteger> getCounts = new ConcurrentHashMap<>();
|
||||||
|
private final int maxRequests;
|
||||||
|
private final int maxGetRequests;
|
||||||
|
|
||||||
|
public IPRateLimitingFilter(int maxRequests, int maxGetRequests) {
|
||||||
|
this.maxRequests = maxRequests;
|
||||||
|
this.maxGetRequests = maxGetRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
if (request instanceof HttpServletRequest) {
|
||||||
|
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||||
|
String method = httpRequest.getMethod();
|
||||||
|
String requestURI = httpRequest.getRequestURI();
|
||||||
|
// 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 (isStaticResource) {
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String clientIp = request.getRemoteAddr();
|
||||||
|
requestCounts.computeIfAbsent(clientIp, k -> new AtomicInteger(0));
|
||||||
|
if (!"GET".equalsIgnoreCase(method)) {
|
||||||
|
|
||||||
|
if (requestCounts.get(clientIp).incrementAndGet() > maxRequests) {
|
||||||
|
// Handle limit exceeded (e.g., send error response)
|
||||||
|
response.getWriter().write("Rate limit exceeded");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (requestCounts.get(clientIp).incrementAndGet() > maxGetRequests) {
|
||||||
|
// Handle limit exceeded (e.g., send error response)
|
||||||
|
response.getWriter().write("GET Rate limit exceeded");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetRequestCounts() {
|
||||||
|
requestCounts.clear();
|
||||||
|
getCounts.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,73 +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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.AttemptCounter;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class LoginAttemptService {
|
||||||
|
|
||||||
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
private int MAX_ATTEMPTS;
|
||||||
|
private long ATTEMPT_INCREMENT_TIME;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
MAX_ATTEMPTS = applicationProperties.getSecurity().getLoginAttemptCount();
|
||||||
|
ATTEMPT_INCREMENT_TIME =
|
||||||
|
TimeUnit.MINUTES.toMillis(
|
||||||
|
applicationProperties.getSecurity().getLoginResetTimeMinutes());
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ConcurrentHashMap<String, AttemptCounter> attemptsCache =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public void loginSucceeded(String key) {
|
||||||
|
attemptsCache.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean loginAttemptCheck(String key) {
|
||||||
|
attemptsCache.compute(
|
||||||
|
key,
|
||||||
|
(k, attemptCounter) -> {
|
||||||
|
if (attemptCounter == null
|
||||||
|
|| attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) {
|
||||||
|
return new AttemptCounter();
|
||||||
|
} else {
|
||||||
|
attemptCounter.increment();
|
||||||
|
return attemptCounter;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return attemptsCache.get(key).getAttemptCount() >= MAX_ATTEMPTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBlocked(String key) {
|
||||||
|
AttemptCounter attemptCounter = attemptsCache.get(key);
|
||||||
|
if (attemptCounter != null) {
|
||||||
|
return attemptCounter.getAttemptCount() >= MAX_ATTEMPTS;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class RateLimitResetScheduler {
|
||||||
|
|
||||||
|
private final IPRateLimitingFilter rateLimitingFilter;
|
||||||
|
|
||||||
|
public RateLimitResetScheduler(IPRateLimitingFilter rateLimitingFilter) {
|
||||||
|
this.rateLimitingFilter = rateLimitingFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(cron = "0 0 0 * * MON") // At 00:00 every Monday TODO: configurable
|
||||||
|
public void resetRateLimit() {
|
||||||
|
rateLimitingFilter.resetRequestCounts();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ 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.EnableGlobalMethodSecurity;
|
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;
|
||||||
@@ -15,78 +15,113 @@ 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.util.matcher.AntPathRequestMatcher;
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
|
||||||
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity()
|
@EnableWebSecurity()
|
||||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
@EnableMethodSecurity
|
||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
@Autowired
|
@Autowired private UserDetailsService userDetailsService;
|
||||||
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
|
@Autowired private UserAuthenticationFilter userAuthenticationFilter;
|
||||||
private UserAuthenticationFilter userAuthenticationFilter;
|
|
||||||
|
@Autowired private LoginAttemptService loginAttemptService;
|
||||||
@Autowired
|
|
||||||
private FirstLoginFilter firstLoginFilter;
|
@Autowired private FirstLoginFilter firstLoginFilter;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
if(loginEnabledValue) {
|
if (loginEnabledValue) {
|
||||||
|
|
||||||
http.csrf(csrf -> csrf.disable());
|
http.csrf(csrf -> csrf.disable());
|
||||||
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||||
http
|
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
.formLogin(formLogin -> formLogin
|
http.formLogin(
|
||||||
.loginPage("/login")
|
formLogin ->
|
||||||
.defaultSuccessUrl("/")
|
formLogin
|
||||||
.failureHandler(new CustomAuthenticationFailureHandler())
|
.loginPage("/login")
|
||||||
.permitAll()
|
.successHandler(
|
||||||
)
|
new CustomAuthenticationSuccessHandler())
|
||||||
.logout(logout -> logout
|
.defaultSuccessUrl("/")
|
||||||
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
|
.failureHandler(
|
||||||
.logoutSuccessUrl("/login?logout=true")
|
new CustomAuthenticationFailureHandler(
|
||||||
.invalidateHttpSession(true) // Invalidate session
|
loginAttemptService))
|
||||||
.deleteCookies("JSESSIONID", "remember-me")
|
.permitAll())
|
||||||
).rememberMe(rememberMeConfigurer -> rememberMeConfigurer // Use the configurator directly
|
.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()))
|
||||||
.key("uniqueAndSecret")
|
.logout(
|
||||||
.tokenRepository(persistentTokenRepository())
|
logout ->
|
||||||
.tokenValiditySeconds(1209600) // 2 weeks
|
logout.logoutRequestMatcher(
|
||||||
)
|
new AntPathRequestMatcher("/logout"))
|
||||||
.authorizeHttpRequests(authz -> authz
|
.logoutSuccessUrl("/login?logout=true")
|
||||||
.requestMatchers(req -> req.getRequestURI().startsWith("/login") || req.getRequestURI().endsWith(".svg") || req.getRequestURI().startsWith("/register") || req.getRequestURI().startsWith("/error") || req.getRequestURI().startsWith("/images/") || req.getRequestURI().startsWith("/public/") || req.getRequestURI().startsWith("/css/") || req.getRequestURI().startsWith("/js/"))
|
.invalidateHttpSession(true) // Invalidate session
|
||||||
.permitAll()
|
.deleteCookies("JSESSIONID", "remember-me"))
|
||||||
.anyRequest().authenticated()
|
.rememberMe(
|
||||||
)
|
rememberMeConfigurer ->
|
||||||
.userDetailsService(userDetailsService)
|
rememberMeConfigurer // Use the configurator directly
|
||||||
.authenticationProvider(authenticationProvider());
|
.key("uniqueAndSecret")
|
||||||
} else {
|
.tokenRepository(persistentTokenRepository())
|
||||||
http.csrf(csrf -> csrf.disable())
|
.tokenValiditySeconds(1209600) // 2 weeks
|
||||||
.authorizeHttpRequests(authz -> authz
|
)
|
||||||
.anyRequest().permitAll()
|
.authorizeHttpRequests(
|
||||||
);
|
authz ->
|
||||||
}
|
authz.requestMatchers(
|
||||||
|
req -> {
|
||||||
|
String uri = req.getRequestURI();
|
||||||
|
String contextPath = req.getContextPath();
|
||||||
|
|
||||||
|
// Remove the context path from the URI
|
||||||
|
String trimmedUri =
|
||||||
|
uri.startsWith(contextPath)
|
||||||
|
? uri.substring(
|
||||||
|
contextPath
|
||||||
|
.length())
|
||||||
|
: uri;
|
||||||
|
|
||||||
|
return trimmedUri.startsWith("/login")
|
||||||
|
|| trimmedUri.endsWith(".svg")
|
||||||
|
|| trimmedUri.startsWith(
|
||||||
|
"/register")
|
||||||
|
|| trimmedUri.startsWith("/error")
|
||||||
|
|| trimmedUri.startsWith("/images/")
|
||||||
|
|| trimmedUri.startsWith("/public/")
|
||||||
|
|| trimmedUri.startsWith("/css/")
|
||||||
|
|| trimmedUri.startsWith("/js/");
|
||||||
|
})
|
||||||
|
.permitAll()
|
||||||
|
.anyRequest()
|
||||||
|
.authenticated())
|
||||||
|
.userDetailsService(userDetailsService)
|
||||||
|
.authenticationProvider(authenticationProvider());
|
||||||
|
} else {
|
||||||
|
http.csrf(csrf -> csrf.disable())
|
||||||
|
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||||
|
}
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public IPRateLimitingFilter rateLimitingFilter() {
|
||||||
|
int maxRequestsPerIp = 1000000; // Example limit TODO add config level
|
||||||
|
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public DaoAuthenticationProvider authenticationProvider() {
|
public DaoAuthenticationProvider authenticationProvider() {
|
||||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
||||||
@@ -94,13 +129,9 @@ public class SecurityConfiguration {
|
|||||||
authProvider.setPasswordEncoder(passwordEncoder());
|
authProvider.setPasswordEncoder(passwordEncoder());
|
||||||
return authProvider;
|
return authProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PersistentTokenRepository persistentTokenRepository() {
|
public PersistentTokenRepository persistentTokenRepository() {
|
||||||
return new JPATokenRepositoryImpl();
|
return new JPATokenRepositoryImpl();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,32 +19,29 @@ 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
|
@Component
|
||||||
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
@Autowired
|
@Autowired private UserDetailsService userDetailsService;
|
||||||
private UserDetailsService userDetailsService;
|
|
||||||
|
@Autowired @Lazy private UserService userService;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Lazy
|
|
||||||
private UserService userService;
|
|
||||||
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("loginEnabled")
|
@Qualifier("loginEnabled")
|
||||||
public boolean loginEnabledValue;
|
public boolean loginEnabledValue;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request,
|
protected void doFilterInternal(
|
||||||
HttpServletResponse response,
|
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
FilterChain filterChain) throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
|
||||||
if (!loginEnabledValue) {
|
if (!loginEnabledValue) {
|
||||||
// If login is not enabled, just pass all requests without authentication
|
// If login is not enabled, just pass all requests without authentication
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
// Check for API key in the request headers if no authentication exists
|
// Check for API key in the request headers if no authentication exists
|
||||||
@@ -52,15 +49,17 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
String apiKey = request.getHeader("X-API-Key");
|
String apiKey = request.getHeader("X-API-Key");
|
||||||
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
||||||
try {
|
try {
|
||||||
// Use API key to authenticate. This requires you to have an authentication provider for API keys.
|
// Use API key to authenticate. This requires you to have an authentication
|
||||||
UserDetails userDetails = userService.loadUserByApiKey(apiKey);
|
// provider for API keys.
|
||||||
if(userDetails == null)
|
UserDetails userDetails = userService.loadUserByApiKey(apiKey);
|
||||||
{
|
if (userDetails == null) {
|
||||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
response.getWriter().write("Invalid API Key.");
|
response.getWriter().write("Invalid API Key.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
authentication = 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
|
||||||
@@ -73,32 +72,38 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
// 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();
|
||||||
if ("GET".equalsIgnoreCase(method) && !"/login".equals(requestURI)) {
|
String contextPath = request.getContextPath();
|
||||||
response.sendRedirect("/login"); // redirect to the login page
|
|
||||||
return;
|
if ("GET".equalsIgnoreCase(method) && !(contextPath + "/login").equals(requestURI)) {
|
||||||
|
response.sendRedirect(contextPath + "/login"); // redirect to the login page
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
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");
|
response.getWriter()
|
||||||
return;
|
.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");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
|
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
|
||||||
String uri = request.getRequestURI();
|
String uri = request.getRequestURI();
|
||||||
|
String contextPath = request.getContextPath();
|
||||||
String[] permitAllPatterns = {
|
String[] permitAllPatterns = {
|
||||||
"/login",
|
contextPath + "/login",
|
||||||
"/register",
|
contextPath + "/register",
|
||||||
"/error",
|
contextPath + "/error",
|
||||||
"/images/",
|
contextPath + "/images/",
|
||||||
"/public/",
|
contextPath + "/public/",
|
||||||
"/css/",
|
contextPath + "/css/",
|
||||||
"/js/"
|
contextPath + "/js/",
|
||||||
|
contextPath + "/pdfjs/",
|
||||||
|
contextPath + "/site.webmanifest"
|
||||||
};
|
};
|
||||||
|
|
||||||
for (String pattern : permitAllPatterns) {
|
for (String pattern : permitAllPatterns) {
|
||||||
@@ -109,5 +114,4 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,28 +20,29 @@ 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.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.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>();
|
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>();
|
||||||
private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
|
private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@Autowired
|
@Autowired private UserDetailsService userDetailsService;
|
||||||
private UserDetailsService userDetailsService;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("rateLimit")
|
@Qualifier("rateLimit")
|
||||||
public boolean rateLimit;
|
public boolean rateLimit;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request,
|
protected void doFilterInternal(
|
||||||
HttpServletResponse response,
|
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
FilterChain filterChain) throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
if (!rateLimit) {
|
if (!rateLimit) {
|
||||||
// If rateLimit is not enabled, just pass all requests without rate limiting
|
// If rateLimit is not enabled, just pass all requests without rate limiting
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
@@ -60,7 +61,8 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
|||||||
// Check for API key in the request headers
|
// Check for API key in the request headers
|
||||||
String apiKey = request.getHeader("X-API-Key");
|
String apiKey = request.getHeader("X-API-Key");
|
||||||
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
||||||
identifier = "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
|
identifier =
|
||||||
|
"API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
|
||||||
} else {
|
} else {
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
@@ -74,14 +76,27 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
|||||||
identifier = request.getRemoteAddr();
|
identifier = request.getRemoteAddr();
|
||||||
}
|
}
|
||||||
|
|
||||||
Role userRole = getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
|
Role userRole =
|
||||||
|
getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
|
||||||
|
|
||||||
if (request.getHeader("X-API-Key") != null) {
|
if (request.getHeader("X-API-Key") != null) {
|
||||||
// It's an API call
|
// It's an API call
|
||||||
processRequest(userRole.getApiCallsPerDay(), identifier, apiBuckets, request, response, filterChain);
|
processRequest(
|
||||||
|
userRole.getApiCallsPerDay(),
|
||||||
|
identifier,
|
||||||
|
apiBuckets,
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
filterChain);
|
||||||
} else {
|
} else {
|
||||||
// It's a Web UI call
|
// It's a Web UI call
|
||||||
processRequest(userRole.getWebCallsPerDay(), identifier, webBuckets, request, response, filterChain);
|
processRequest(
|
||||||
|
userRole.getWebCallsPerDay(),
|
||||||
|
identifier,
|
||||||
|
webBuckets,
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
filterChain);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,8 +113,13 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
|||||||
throw new IllegalStateException("User does not have a valid role.");
|
throw new IllegalStateException("User does not have a valid role.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processRequest(int limitPerDay, String identifier, Map<String, Bucket> buckets,
|
private void processRequest(
|
||||||
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
int limitPerDay,
|
||||||
|
String identifier,
|
||||||
|
Map<String, Bucket> buckets,
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
FilterChain filterChain)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay));
|
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay));
|
||||||
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
|
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
|
||||||
@@ -116,10 +136,8 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Bucket createUserBucket(int limitPerDay) {
|
private Bucket createUserBucket(int limitPerDay) {
|
||||||
Bandwidth limit = Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1)));
|
Bandwidth limit =
|
||||||
|
Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1)));
|
||||||
return Bucket.builder().addLimit(limit).build();
|
return Bucket.builder().addLimit(limit).build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -16,41 +17,40 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
|||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
||||||
import stirling.software.SPDF.model.Authority;
|
import stirling.software.SPDF.model.Authority;
|
||||||
|
import stirling.software.SPDF.model.Role;
|
||||||
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
|
|
||||||
public class UserService {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private UserRepository userRepository;
|
|
||||||
|
|
||||||
@Autowired
|
@Service
|
||||||
private PasswordEncoder passwordEncoder;
|
public class UserService implements UserServiceInterface {
|
||||||
|
|
||||||
|
@Autowired private UserRepository userRepository;
|
||||||
|
|
||||||
|
@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() {
|
private String generateApiKey() {
|
||||||
String apiKey;
|
String apiKey;
|
||||||
do {
|
do {
|
||||||
@@ -60,9 +60,11 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public User addApiKeyToUser(String username) {
|
public User addApiKeyToUser(String username) {
|
||||||
User user = userRepository.findByUsername(username)
|
User user =
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
userRepository
|
||||||
|
.findByUsername(username)
|
||||||
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||||
|
|
||||||
user.setApiKey(generateApiKey());
|
user.setApiKey(generateApiKey());
|
||||||
return userRepository.save(user);
|
return userRepository.save(user);
|
||||||
}
|
}
|
||||||
@@ -72,8 +74,10 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getApiKeyForUser(String username) {
|
public String getApiKeyForUser(String username) {
|
||||||
User user = userRepository.findByUsername(username)
|
User user =
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
userRepository
|
||||||
|
.findByUsername(username)
|
||||||
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||||
return user.getApiKey();
|
return user.getApiKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,27 +88,25 @@ public class UserService {
|
|||||||
public User getUserByApiKey(String apiKey) {
|
public User getUserByApiKey(String apiKey) {
|
||||||
return userRepository.findByApiKey(apiKey);
|
return userRepository.findByApiKey(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserDetails loadUserByApiKey(String apiKey) {
|
public UserDetails loadUserByApiKey(String apiKey) {
|
||||||
User userOptional = userRepository.findByApiKey(apiKey);
|
User userOptional = userRepository.findByApiKey(apiKey);
|
||||||
if (userOptional != null) {
|
if (userOptional != null) {
|
||||||
User user = userOptional;
|
User user = userOptional;
|
||||||
// Convert your User entity to a UserDetails object with authorities
|
// Convert your User entity to a UserDetails object with authorities
|
||||||
return new org.springframework.security.core.userdetails.User(
|
return new org.springframework.security.core.userdetails.User(
|
||||||
user.getUsername(),
|
user.getUsername(),
|
||||||
user.getPassword(), // you might not need this for API key auth
|
user.getPassword(), // you might not need this for API key auth
|
||||||
getAuthorities(user)
|
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);
|
||||||
@@ -122,7 +124,7 @@ public class UserService {
|
|||||||
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);
|
||||||
@@ -132,37 +134,42 @@ public class UserService {
|
|||||||
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()) {
|
||||||
userRepository.delete(userOpt.get());
|
for (Authority authority : userOpt.get().getAuthorities()) {
|
||||||
}
|
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) {
|
||||||
@@ -178,13 +185,12 @@ public class UserService {
|
|||||||
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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.io.MemoryUsageSetting;
|
||||||
|
import org.apache.pdfbox.multipdf.PDFMergerUtility;
|
||||||
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.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -23,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.general.MergePdfsRequest;
|
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@@ -33,83 +36,97 @@ 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 (PDPage page : doc.getPages()) {
|
|
||||||
mergedDoc.addPage(page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mergedDoc;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Comparator<MultipartFile> getSortComparator(String sortType) {
|
|
||||||
switch (sortType) {
|
|
||||||
case "byFileName":
|
|
||||||
return Comparator.comparing(MultipartFile::getOriginalFilename);
|
|
||||||
case "byDateModified":
|
|
||||||
return (file1, file2) -> {
|
|
||||||
try {
|
|
||||||
BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class);
|
|
||||||
BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class);
|
|
||||||
return attr1.lastModifiedTime().compareTo(attr2.lastModifiedTime());
|
|
||||||
} catch (IOException e) {
|
|
||||||
return 0; // If there's an error, treat them as equal
|
|
||||||
}
|
|
||||||
};
|
|
||||||
case "byDateCreated":
|
|
||||||
return (file1, file2) -> {
|
|
||||||
try {
|
|
||||||
BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class);
|
|
||||||
BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class);
|
|
||||||
return attr1.creationTime().compareTo(attr2.creationTime());
|
|
||||||
} catch (IOException e) {
|
|
||||||
return 0; // If there's an error, treat them as equal
|
|
||||||
}
|
|
||||||
};
|
|
||||||
case "byPDFTitle":
|
|
||||||
return (file1, file2) -> {
|
|
||||||
try (PDDocument doc1 = PDDocument.load(file1.getInputStream());
|
|
||||||
PDDocument doc2 = PDDocument.load(file2.getInputStream())) {
|
|
||||||
String title1 = doc1.getDocumentInformation().getTitle();
|
|
||||||
String title2 = doc2.getDocumentInformation().getTitle();
|
|
||||||
return title1.compareTo(title2);
|
|
||||||
} catch (IOException e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
case "orderProvided":
|
|
||||||
default:
|
|
||||||
return (file1, file2) -> 0; // Default is the order provided
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
|
|
||||||
@Operation(summary = "Merge multiple PDF files into one",
|
|
||||||
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 {
|
|
||||||
|
|
||||||
MultipartFile[] files = form.getFileInput();
|
|
||||||
Arrays.sort(files, getSortComparator(form.getSortType()));
|
|
||||||
|
|
||||||
List<PDDocument> documents = new ArrayList<>();
|
|
||||||
for (MultipartFile file : files) {
|
|
||||||
try (InputStream is = file.getInputStream()) {
|
|
||||||
documents.add(PDDocument.load(is));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try (PDDocument mergedDoc = mergeDocuments(documents)) {
|
|
||||||
ResponseEntity<byte[]> response = WebResponseUtils.pdfDocToWebResponse(mergedDoc, files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf");
|
|
||||||
return response;
|
|
||||||
} finally {
|
|
||||||
for (PDDocument doc : documents) {
|
for (PDDocument doc : documents) {
|
||||||
if (doc != null) {
|
for (PDPage page : doc.getPages()) {
|
||||||
doc.close();
|
mergedDoc.addPage(page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return mergedDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Comparator<MultipartFile> getSortComparator(String sortType) {
|
||||||
|
switch (sortType) {
|
||||||
|
case "byFileName":
|
||||||
|
return Comparator.comparing(MultipartFile::getOriginalFilename);
|
||||||
|
case "byDateModified":
|
||||||
|
return (file1, file2) -> {
|
||||||
|
try {
|
||||||
|
BasicFileAttributes attr1 =
|
||||||
|
Files.readAttributes(
|
||||||
|
Paths.get(file1.getOriginalFilename()),
|
||||||
|
BasicFileAttributes.class);
|
||||||
|
BasicFileAttributes attr2 =
|
||||||
|
Files.readAttributes(
|
||||||
|
Paths.get(file2.getOriginalFilename()),
|
||||||
|
BasicFileAttributes.class);
|
||||||
|
return attr1.lastModifiedTime().compareTo(attr2.lastModifiedTime());
|
||||||
|
} catch (IOException e) {
|
||||||
|
return 0; // If there's an error, treat them as equal
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case "byDateCreated":
|
||||||
|
return (file1, file2) -> {
|
||||||
|
try {
|
||||||
|
BasicFileAttributes attr1 =
|
||||||
|
Files.readAttributes(
|
||||||
|
Paths.get(file1.getOriginalFilename()),
|
||||||
|
BasicFileAttributes.class);
|
||||||
|
BasicFileAttributes attr2 =
|
||||||
|
Files.readAttributes(
|
||||||
|
Paths.get(file2.getOriginalFilename()),
|
||||||
|
BasicFileAttributes.class);
|
||||||
|
return attr1.creationTime().compareTo(attr2.creationTime());
|
||||||
|
} catch (IOException e) {
|
||||||
|
return 0; // If there's an error, treat them as equal
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case "byPDFTitle":
|
||||||
|
return (file1, file2) -> {
|
||||||
|
try (PDDocument doc1 = PDDocument.load(file1.getInputStream());
|
||||||
|
PDDocument doc2 = PDDocument.load(file2.getInputStream())) {
|
||||||
|
String title1 = doc1.getDocumentInformation().getTitle();
|
||||||
|
String title2 = doc2.getDocumentInformation().getTitle();
|
||||||
|
return title1.compareTo(title2);
|
||||||
|
} catch (IOException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case "orderProvided":
|
||||||
|
default:
|
||||||
|
return (file1, file2) -> 0; // Default is the order provided
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
|
||||||
|
@Operation(
|
||||||
|
summary = "Merge multiple PDF files into one",
|
||||||
|
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 {
|
||||||
|
MultipartFile[] files = form.getFileInput();
|
||||||
|
Arrays.sort(files, getSortComparator(form.getSortType()));
|
||||||
|
|
||||||
|
PDFMergerUtility mergedDoc = new PDFMergerUtility();
|
||||||
|
ByteArrayOutputStream docOutputstream = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
for (MultipartFile file : files) {
|
||||||
|
mergedDoc.addSource(new ByteArrayInputStream(file.getBytes()));
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedDoc.setDestinationFileName(
|
||||||
|
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf");
|
||||||
|
mergedDoc.setDestinationStream(docOutputstream);
|
||||||
|
mergedDoc.mergeDocuments(MemoryUsageSetting.setupMainMemoryOnly());
|
||||||
|
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
docOutputstream.toByteArray(), mergedDoc.getDestinationFileName());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
logger.error("Error in merge pdf process", ex);
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.multipdf.Overlay;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
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.general.OverlayPdfsRequest;
|
||||||
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/general")
|
||||||
|
@Tag(name = "General", description = "General APIs")
|
||||||
|
public class PdfOverlayController {
|
||||||
|
|
||||||
|
@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")
|
||||||
|
public ResponseEntity<byte[]> overlayPdfs(@ModelAttribute OverlayPdfsRequest request)
|
||||||
|
throws IOException {
|
||||||
|
MultipartFile baseFile = request.getFileInput();
|
||||||
|
int overlayPos = request.getOverlayPosition();
|
||||||
|
|
||||||
|
MultipartFile[] overlayFiles = request.getOverlayFiles();
|
||||||
|
File[] overlayPdfFiles = new File[overlayFiles.length];
|
||||||
|
List<File> tempFiles = new ArrayList<>(); // List to keep track of temporary files
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < overlayFiles.length; i++) {
|
||||||
|
overlayPdfFiles[i] = GeneralUtils.multipartToFile(overlayFiles[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
String mode = request.getOverlayMode(); // "SequentialOverlay", "InterleavedOverlay",
|
||||||
|
// "FixedRepeatOverlay"
|
||||||
|
int[] counts = request.getCounts(); // Used for FixedRepeatOverlay mode
|
||||||
|
|
||||||
|
try (PDDocument basePdf = PDDocument.load(baseFile.getInputStream());
|
||||||
|
Overlay overlay = new Overlay()) {
|
||||||
|
Map<Integer, String> overlayGuide =
|
||||||
|
prepareOverlayGuide(
|
||||||
|
basePdf.getNumberOfPages(),
|
||||||
|
overlayPdfFiles,
|
||||||
|
mode,
|
||||||
|
counts,
|
||||||
|
tempFiles);
|
||||||
|
|
||||||
|
overlay.setInputPDF(basePdf);
|
||||||
|
if (overlayPos == 0) {
|
||||||
|
overlay.setOverlayPosition(Overlay.Position.FOREGROUND);
|
||||||
|
} else {
|
||||||
|
overlay.setOverlayPosition(Overlay.Position.BACKGROUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
overlay.overlay(overlayGuide).save(outputStream);
|
||||||
|
byte[] data = outputStream.toByteArray();
|
||||||
|
String outputFilename =
|
||||||
|
baseFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_overlayed.pdf"; // Remove file extension and append .pdf
|
||||||
|
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
data, outputFilename, MediaType.APPLICATION_PDF);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
for (File overlayPdfFile : overlayPdfFiles) {
|
||||||
|
if (overlayPdfFile != null) {
|
||||||
|
overlayPdfFile.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (File tempFile : tempFiles) { // Delete temporary files
|
||||||
|
if (tempFile != null) {
|
||||||
|
tempFile.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Integer, String> prepareOverlayGuide(
|
||||||
|
int basePageCount, File[] overlayFiles, String mode, int[] counts, List<File> tempFiles)
|
||||||
|
throws IOException {
|
||||||
|
Map<Integer, String> overlayGuide = new HashMap<>();
|
||||||
|
switch (mode) {
|
||||||
|
case "SequentialOverlay":
|
||||||
|
sequentialOverlay(overlayGuide, overlayFiles, basePageCount, tempFiles);
|
||||||
|
break;
|
||||||
|
case "InterleavedOverlay":
|
||||||
|
interleavedOverlay(overlayGuide, overlayFiles, basePageCount);
|
||||||
|
break;
|
||||||
|
case "FixedRepeatOverlay":
|
||||||
|
fixedRepeatOverlay(overlayGuide, overlayFiles, counts, basePageCount);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid overlay mode");
|
||||||
|
}
|
||||||
|
return overlayGuide;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sequentialOverlay(
|
||||||
|
Map<Integer, String> overlayGuide,
|
||||||
|
File[] overlayFiles,
|
||||||
|
int basePageCount,
|
||||||
|
List<File> tempFiles)
|
||||||
|
throws IOException {
|
||||||
|
int overlayFileIndex = 0;
|
||||||
|
int pageCountInCurrentOverlay = 0;
|
||||||
|
|
||||||
|
for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) {
|
||||||
|
if (pageCountInCurrentOverlay == 0
|
||||||
|
|| pageCountInCurrentOverlay
|
||||||
|
>= getNumberOfPages(overlayFiles[overlayFileIndex])) {
|
||||||
|
pageCountInCurrentOverlay = 0;
|
||||||
|
overlayFileIndex = (overlayFileIndex + 1) % overlayFiles.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (PDDocument overlayPdf = PDDocument.load(overlayFiles[overlayFileIndex])) {
|
||||||
|
PDDocument singlePageDocument = new PDDocument();
|
||||||
|
singlePageDocument.addPage(overlayPdf.getPage(pageCountInCurrentOverlay));
|
||||||
|
File tempFile = File.createTempFile("overlay-page-", ".pdf");
|
||||||
|
singlePageDocument.save(tempFile);
|
||||||
|
singlePageDocument.close();
|
||||||
|
|
||||||
|
overlayGuide.put(basePageIndex, tempFile.getAbsolutePath());
|
||||||
|
tempFiles.add(tempFile); // Keep track of the temporary file for cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
pageCountInCurrentOverlay++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getNumberOfPages(File file) throws IOException {
|
||||||
|
try (PDDocument doc = PDDocument.load(file)) {
|
||||||
|
return doc.getNumberOfPages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void interleavedOverlay(
|
||||||
|
Map<Integer, String> overlayGuide, File[] overlayFiles, int basePageCount)
|
||||||
|
throws IOException {
|
||||||
|
for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) {
|
||||||
|
File overlayFile = overlayFiles[(basePageIndex - 1) % overlayFiles.length];
|
||||||
|
|
||||||
|
// Load the overlay document to check its page count
|
||||||
|
try (PDDocument overlayPdf = PDDocument.load(overlayFile)) {
|
||||||
|
int overlayPageCount = overlayPdf.getNumberOfPages();
|
||||||
|
if ((basePageIndex - 1) % overlayPageCount < overlayPageCount) {
|
||||||
|
overlayGuide.put(basePageIndex, overlayFile.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fixedRepeatOverlay(
|
||||||
|
Map<Integer, String> overlayGuide, File[] overlayFiles, int[] counts, int basePageCount)
|
||||||
|
throws IOException {
|
||||||
|
if (overlayFiles.length != counts.length) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Counts array length must match the number of overlay files");
|
||||||
|
}
|
||||||
|
int currentPage = 1;
|
||||||
|
for (int i = 0; i < overlayFiles.length; i++) {
|
||||||
|
File overlayFile = overlayFiles[i];
|
||||||
|
int repeatCount = counts[i];
|
||||||
|
|
||||||
|
// Load the overlay document to check its page count
|
||||||
|
try (PDDocument overlayPdf = PDDocument.load(overlayFile)) {
|
||||||
|
int overlayPageCount = overlayPdf.getNumberOfPages();
|
||||||
|
for (int j = 0; j < repeatCount; j++) {
|
||||||
|
for (int page = 0; page < overlayPageCount; page++) {
|
||||||
|
if (currentPage > basePageCount) break;
|
||||||
|
overlayGuide.put(currentPage++, overlayFile.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional classes like OverlayPdfsRequest, WebResponseUtils, etc. are assumed to be defined
|
||||||
|
// elsewhere.
|
||||||
@@ -12,208 +12,209 @@ 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.RequestParam;
|
|
||||||
import org.springframework.web.bind.annotation.RequestPart;
|
|
||||||
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.Parameter;
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
|
import org.apache.pdfbox.util.Matrix;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
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.SplitPdfBySectionsRequest;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/general")
|
||||||
|
@Tag(name = "General", description = "General APIs")
|
||||||
|
public class SplitPdfBySectionsController {
|
||||||
|
|
||||||
|
@PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data")
|
||||||
|
@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")
|
||||||
|
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfBySectionsRequest request)
|
||||||
|
throws Exception {
|
||||||
|
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
|
||||||
|
|
||||||
|
MultipartFile file = request.getFileInput();
|
||||||
|
PDDocument sourceDocument = PDDocument.load(file.getInputStream());
|
||||||
|
|
||||||
|
// Process the PDF based on split parameters
|
||||||
|
int horiz = request.getHorizontalDivisions() + 1;
|
||||||
|
int verti = request.getVerticalDivisions() + 1;
|
||||||
|
|
||||||
|
List<PDDocument> splitDocuments = splitPdfPages(sourceDocument, verti, horiz);
|
||||||
|
for (PDDocument doc : splitDocuments) {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
doc.save(baos);
|
||||||
|
doc.close();
|
||||||
|
splitDocumentsBoas.add(baos);
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceDocument.close();
|
||||||
|
|
||||||
|
Path zipFile = Files.createTempFile("split_documents", ".zip");
|
||||||
|
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
|
||||||
|
byte[] data;
|
||||||
|
|
||||||
|
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
|
||||||
|
int pageNum = 1;
|
||||||
|
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
|
||||||
|
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
|
||||||
|
int sectionNum = (i % (horiz * verti)) + 1;
|
||||||
|
String fileName = filename + "_" + pageNum + "_" + sectionNum + ".pdf";
|
||||||
|
byte[] pdf = baos.toByteArray();
|
||||||
|
ZipEntry pdfEntry = new ZipEntry(fileName);
|
||||||
|
zipOut.putNextEntry(pdfEntry);
|
||||||
|
zipOut.write(pdf);
|
||||||
|
zipOut.closeEntry();
|
||||||
|
|
||||||
|
if (sectionNum == horiz * verti) pageNum++;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
data = Files.readAllBytes(zipFile);
|
||||||
|
Files.delete(zipFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PDDocument> splitPdfPages(
|
||||||
|
PDDocument document, int horizontalDivisions, int verticalDivisions)
|
||||||
|
throws IOException {
|
||||||
|
List<PDDocument> splitDocuments = new ArrayList<>();
|
||||||
|
|
||||||
|
for (PDPage originalPage : document.getPages()) {
|
||||||
|
PDRectangle originalMediaBox = originalPage.getMediaBox();
|
||||||
|
float width = originalMediaBox.getWidth();
|
||||||
|
float height = originalMediaBox.getHeight();
|
||||||
|
float subPageWidth = width / horizontalDivisions;
|
||||||
|
float subPageHeight = height / verticalDivisions;
|
||||||
|
|
||||||
|
LayerUtility layerUtility = new LayerUtility(document);
|
||||||
|
|
||||||
|
for (int i = 0; i < horizontalDivisions; i++) {
|
||||||
|
for (int j = 0; j < verticalDivisions; j++) {
|
||||||
|
PDDocument subDoc = new PDDocument();
|
||||||
|
PDPage subPage = new PDPage(new PDRectangle(subPageWidth, subPageHeight));
|
||||||
|
subDoc.addPage(subPage);
|
||||||
|
|
||||||
|
PDFormXObject form =
|
||||||
|
layerUtility.importPageAsForm(
|
||||||
|
document, document.getPages().indexOf(originalPage));
|
||||||
|
|
||||||
|
try (PDPageContentStream contentStream =
|
||||||
|
new PDPageContentStream(subDoc, subPage)) {
|
||||||
|
// Set clipping area and position
|
||||||
|
float translateX = -subPageWidth * i;
|
||||||
|
float translateY = height - subPageHeight * (verticalDivisions - j);
|
||||||
|
|
||||||
|
contentStream.saveGraphicsState();
|
||||||
|
contentStream.addRect(0, 0, subPageWidth, subPageHeight);
|
||||||
|
contentStream.clip();
|
||||||
|
contentStream.transform(new Matrix(1, 0, 0, 1, translateX, translateY));
|
||||||
|
|
||||||
|
// Draw the form
|
||||||
|
contentStream.drawForm(form);
|
||||||
|
contentStream.restoreGraphicsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
splitDocuments.add(subDoc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return splitDocuments;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
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.general.SplitPdfBySizeOrCountRequest;
|
||||||
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/general")
|
||||||
|
@Tag(name = "General", description = "General APIs")
|
||||||
|
public class SplitPdfBySizeController {
|
||||||
|
|
||||||
|
@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"
|
||||||
|
+ " 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();
|
||||||
|
PDDocument sourceDocument = PDDocument.load(file.getInputStream());
|
||||||
|
|
||||||
|
// 0 = size, 1 = page count, 2 = doc count
|
||||||
|
int type = request.getSplitType();
|
||||||
|
String value = request.getSplitValue();
|
||||||
|
|
||||||
|
if (type == 0) { // Split by size
|
||||||
|
long maxBytes = GeneralUtils.convertSizeToBytes(value);
|
||||||
|
long currentSize = 0;
|
||||||
|
PDDocument currentDoc = new PDDocument();
|
||||||
|
|
||||||
|
for (PDPage page : sourceDocument.getPages()) {
|
||||||
|
ByteArrayOutputStream pageOutputStream = new ByteArrayOutputStream();
|
||||||
|
PDDocument tempDoc = new PDDocument();
|
||||||
|
tempDoc.addPage(page);
|
||||||
|
tempDoc.save(pageOutputStream);
|
||||||
|
tempDoc.close();
|
||||||
|
|
||||||
|
long pageSize = pageOutputStream.size();
|
||||||
|
if (currentSize + pageSize > maxBytes) {
|
||||||
|
// Save and reset current document
|
||||||
|
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
|
||||||
|
currentDoc = new PDDocument();
|
||||||
|
currentSize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDoc.addPage(page);
|
||||||
|
currentSize += pageSize;
|
||||||
|
}
|
||||||
|
// Add the last document if it contains any pages
|
||||||
|
if (currentDoc.getPages().getCount() != 0) {
|
||||||
|
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
|
||||||
|
}
|
||||||
|
} else if (type == 1) { // Split by page count
|
||||||
|
int pageCount = Integer.parseInt(value);
|
||||||
|
int currentPageCount = 0;
|
||||||
|
PDDocument currentDoc = new PDDocument();
|
||||||
|
|
||||||
|
for (PDPage page : sourceDocument.getPages()) {
|
||||||
|
currentDoc.addPage(page);
|
||||||
|
currentPageCount++;
|
||||||
|
|
||||||
|
if (currentPageCount == pageCount) {
|
||||||
|
// Save and reset current document
|
||||||
|
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
|
||||||
|
currentDoc = new PDDocument();
|
||||||
|
currentPageCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add the last document if it contains any pages
|
||||||
|
if (currentDoc.getPages().getCount() != 0) {
|
||||||
|
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
|
||||||
|
}
|
||||||
|
} else if (type == 2) { // Split by doc count
|
||||||
|
int documentCount = Integer.parseInt(value);
|
||||||
|
int totalPageCount = sourceDocument.getNumberOfPages();
|
||||||
|
int pagesPerDocument = totalPageCount / documentCount;
|
||||||
|
int extraPages = totalPageCount % documentCount;
|
||||||
|
int currentPageIndex = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < documentCount; i++) {
|
||||||
|
PDDocument currentDoc = new PDDocument();
|
||||||
|
int pagesToAdd = pagesPerDocument + (i < extraPages ? 1 : 0);
|
||||||
|
|
||||||
|
for (int j = 0; j < pagesToAdd; j++) {
|
||||||
|
currentDoc.addPage(sourceDocument.getPage(currentPageIndex++));
|
||||||
|
}
|
||||||
|
|
||||||
|
splitDocumentsBoas.add(currentDocToByteArray(currentDoc));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Invalid argument for split type");
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceDocument.close();
|
||||||
|
|
||||||
|
Path zipFile = Files.createTempFile("split_documents", ".zip");
|
||||||
|
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
|
||||||
|
byte[] data;
|
||||||
|
|
||||||
|
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
|
||||||
|
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
|
||||||
|
String fileName = filename + "_" + (i + 1) + ".pdf";
|
||||||
|
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
|
||||||
|
byte[] pdf = baos.toByteArray();
|
||||||
|
|
||||||
|
ZipEntry pdfEntry = new ZipEntry(fileName);
|
||||||
|
zipOut.putNextEntry(pdfEntry);
|
||||||
|
zipOut.write(pdf);
|
||||||
|
zipOut.closeEntry();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
data = Files.readAllBytes(zipFile);
|
||||||
|
Files.delete(zipFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ByteArrayOutputStream currentDocToByteArray(PDDocument document) throws IOException {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
document.save(baos);
|
||||||
|
document.close();
|
||||||
|
return baos;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,18 +23,20 @@ import org.springframework.web.servlet.view.RedirectView;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
|
import stirling.software.SPDF.model.Role;
|
||||||
import stirling.software.SPDF.model.User;
|
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')")
|
||||||
@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";
|
||||||
}
|
}
|
||||||
@@ -42,38 +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')")
|
||||||
@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);
|
||||||
@@ -84,36 +89,36 @@ public class UserController {
|
|||||||
return new RedirectView("/login?messageType=credsUpdated");
|
return new RedirectView("/login?messageType=credsUpdated");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,28 +128,30 @@ public class UserController {
|
|||||||
return new RedirectView("/login?messageType=credsUpdated");
|
return new RedirectView("/login?messageType=credsUpdated");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
|
||||||
@@ -154,55 +161,71 @@ public class UserController {
|
|||||||
return new RedirectView("/login?messageType=credsUpdated");
|
return new RedirectView("/login?messageType=credsUpdated");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
|
||||||
|
if (userService.usernameExists(username)) {
|
||||||
|
return new RedirectView("/addUsers?messageType=usernameExists");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Validate the role
|
||||||
|
Role roleEnum = Role.fromString(role);
|
||||||
|
if (roleEnum == Role.INTERNAL_API_USER) {
|
||||||
|
// If the role is INTERNAL_API_USER, reject the request
|
||||||
|
return new RedirectView("/addUsers?messageType=invalidRole");
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// If the role ID is not valid, redirect with an error message
|
||||||
|
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')")
|
||||||
@PostMapping("/get-api-key")
|
@PostMapping("/get-api-key")
|
||||||
public ResponseEntity<String> getApiKey(Principal principal) {
|
public ResponseEntity<String> getApiKey(Principal principal) {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
@@ -216,6 +239,7 @@ public class UserController {
|
|||||||
return ResponseEntity.ok(apiKey);
|
return ResponseEntity.ok(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/update-api-key")
|
@PostMapping("/update-api-key")
|
||||||
public ResponseEntity<String> updateApiKey(Principal principal) {
|
public ResponseEntity<String> updateApiKey(Principal principal) {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
@@ -229,6 +253,4 @@ public class UserController {
|
|||||||
}
|
}
|
||||||
return ResponseEntity.ok(apiKey);
|
return ResponseEntity.ok(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.StringReader;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.zip.ZipEntry;
|
|
||||||
import java.util.zip.ZipInputStream;
|
|
||||||
|
|
||||||
import javax.xml.parsers.DocumentBuilder;
|
|
||||||
import javax.xml.parsers.DocumentBuilderFactory;
|
|
||||||
|
|
||||||
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 org.w3c.dom.Document;
|
|
||||||
import org.w3c.dom.Element;
|
|
||||||
import org.w3c.dom.NodeList;
|
|
||||||
import org.xml.sax.InputSource;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
|
||||||
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
|
|
||||||
@RequestMapping("/api/v1/convert")
|
|
||||||
@Tag(name = "Convert", description = "Convert APIs")
|
|
||||||
public class ConvertEpubToPdf {
|
|
||||||
//TODO
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/epub-to-single-pdf")
|
|
||||||
@Hidden
|
|
||||||
@Operation(
|
|
||||||
summary = "Convert an EPUB file to a single PDF",
|
|
||||||
description = "This endpoint takes an EPUB file input and converts it to a single PDF."
|
|
||||||
)
|
|
||||||
public ResponseEntity<byte[]> epubToSinglePdf(
|
|
||||||
@ModelAttribute GeneralFile request)
|
|
||||||
throws Exception {
|
|
||||||
MultipartFile fileInput = request.getFileInput();
|
|
||||||
if (fileInput == null) {
|
|
||||||
throw new IllegalArgumentException("Please provide an EPUB file for conversion.");
|
|
||||||
}
|
|
||||||
|
|
||||||
String originalFilename = fileInput.getOriginalFilename();
|
|
||||||
if (originalFilename == null || !originalFilename.endsWith(".epub")) {
|
|
||||||
throw new IllegalArgumentException("File must be in .epub format.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, byte[]> epubContents = extractEpubContent(fileInput);
|
|
||||||
List<String> htmlFilesOrder = getHtmlFilesOrderFromOpf(epubContents);
|
|
||||||
|
|
||||||
List<byte[]> individualPdfs = new ArrayList<>();
|
|
||||||
|
|
||||||
for (String htmlFile : htmlFilesOrder) {
|
|
||||||
byte[] htmlContent = epubContents.get(htmlFile);
|
|
||||||
byte[] pdfBytes = FileToPdf.convertHtmlToPdf(htmlContent, htmlFile.replace(".html", ".pdf"));
|
|
||||||
individualPdfs.add(pdfBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pseudo-code to merge individual PDFs into one.
|
|
||||||
byte[] mergedPdfBytes = mergeMultiplePdfsIntoOne(individualPdfs);
|
|
||||||
|
|
||||||
return WebResponseUtils.bytesToWebResponse(mergedPdfBytes, originalFilename.replace(".epub", ".pdf"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assuming a pseudo-code function that merges multiple PDFs into one.
|
|
||||||
private byte[] mergeMultiplePdfsIntoOne(List<byte[]> individualPdfs) {
|
|
||||||
// You can use a library such as PDFBox to perform the merging here.
|
|
||||||
// Return the byte[] of the merged PDF.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, byte[]> extractEpubContent(MultipartFile fileInput) throws IOException {
|
|
||||||
Map<String, byte[]> contentMap = new HashMap<>();
|
|
||||||
|
|
||||||
try (ZipInputStream zis = new ZipInputStream(fileInput.getInputStream())) {
|
|
||||||
ZipEntry zipEntry = zis.getNextEntry();
|
|
||||||
while (zipEntry != null) {
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
byte[] buffer = new byte[1024];
|
|
||||||
int read = 0;
|
|
||||||
while ((read = zis.read(buffer)) != -1) {
|
|
||||||
baos.write(buffer, 0, read);
|
|
||||||
}
|
|
||||||
contentMap.put(zipEntry.getName(), baos.toByteArray());
|
|
||||||
zipEntry = zis.getNextEntry();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return contentMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> getHtmlFilesOrderFromOpf(Map<String, byte[]> epubContents) throws Exception {
|
|
||||||
String opfContent = new String(epubContents.get("OEBPS/content.opf")); // Adjusting for given path
|
|
||||||
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
|
|
||||||
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
|
|
||||||
InputSource is = new InputSource(new StringReader(opfContent));
|
|
||||||
Document doc = dBuilder.parse(is);
|
|
||||||
|
|
||||||
NodeList itemRefs = doc.getElementsByTagName("itemref");
|
|
||||||
List<String> htmlFilesOrder = new ArrayList<>();
|
|
||||||
|
|
||||||
for (int i = 0; i < itemRefs.getLength(); i++) {
|
|
||||||
Element itemRef = (Element) itemRefs.item(i);
|
|
||||||
String idref = itemRef.getAttribute("idref");
|
|
||||||
|
|
||||||
NodeList items = doc.getElementsByTagName("item");
|
|
||||||
for (int j = 0; j < items.getLength(); j++) {
|
|
||||||
Element item = (Element) items.item(j);
|
|
||||||
if (idref.equals(item.getAttribute("id"))) {
|
|
||||||
htmlFilesOrder.add(item.getAttribute("href")); // Fetching the actual href
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return htmlFilesOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,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 +19,30 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@RequestMapping("/api/v1/convert")
|
@RequestMapping("/api/v1/convert")
|
||||||
public class ConvertHtmlToPDF {
|
public class ConvertHtmlToPDF {
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
||||||
|
@Operation(
|
||||||
|
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.")
|
||||||
|
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute GeneralFile request) throws Exception {
|
||||||
|
MultipartFile fileInput = request.getFileInput();
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
if (fileInput == null) {
|
||||||
@Operation(
|
throw new IllegalArgumentException(
|
||||||
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
|
"Please provide an HTML or ZIP file for conversion.");
|
||||||
description = "This endpoint takes an HTML or ZIP file input and converts it to a PDF format."
|
}
|
||||||
)
|
|
||||||
public ResponseEntity<byte[]> HtmlToPdf(
|
|
||||||
@ModelAttribute GeneralFile request)
|
|
||||||
throws Exception {
|
|
||||||
MultipartFile fileInput = request.getFileInput();
|
|
||||||
|
|
||||||
if (fileInput == null) {
|
String originalFilename = fileInput.getOriginalFilename();
|
||||||
throw new IllegalArgumentException("Please provide an HTML or ZIP file for conversion.");
|
if (originalFilename == null
|
||||||
}
|
|| (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) {
|
||||||
|
throw new IllegalArgumentException("File must be either .html or .zip format.");
|
||||||
|
}
|
||||||
|
byte[] pdfBytes = FileToPdf.convertHtmlToPdf(fileInput.getBytes(), originalFilename);
|
||||||
|
|
||||||
String originalFilename = fileInput.getOriginalFilename();
|
String outputFilename =
|
||||||
if (originalFilename == null || (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) {
|
originalFilename.replaceFirst("[.][^.]+$", "")
|
||||||
throw new IllegalArgumentException("File must be either .html or .zip format.");
|
+ ".pdf"; // Remove file extension and append .pdf
|
||||||
}byte[] pdfBytes = FileToPdf.convertHtmlToPdf( fileInput.getBytes(), originalFilename);
|
|
||||||
|
|
||||||
String outputFilename = originalFilename.replaceFirst("[.][^.]+$", "") + ".pdf"; // Remove file extension and append .pdf
|
|
||||||
|
|
||||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
|
||||||
import org.apache.pdfbox.rendering.ImageType;
|
import org.apache.pdfbox.rendering.ImageType;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -19,10 +20,12 @@ 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;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/convert")
|
@RequestMapping("/api/v1/convert")
|
||||||
@Tag(name = "Convert", description = "Convert APIs")
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
@@ -31,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)) {
|
||||||
@@ -52,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();
|
||||||
@@ -63,41 +76,43 @@ 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) {
|
||||||
if (imageFormat.equalsIgnoreCase("PNG"))
|
String mimeType = URLConnection.guessContentTypeFromName("." + imageFormat);
|
||||||
return "image/png";
|
return mimeType.equals("null") ? "application/octet-stream" : mimeType;
|
||||||
else if (imageFormat.equalsIgnoreCase("JPEG") || imageFormat.equalsIgnoreCase("JPG"))
|
|
||||||
return "image/jpeg";
|
|
||||||
else if (imageFormat.equalsIgnoreCase("GIF"))
|
|
||||||
return "image/gif";
|
|
||||||
else
|
|
||||||
return "application/octet-stream";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,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 +21,16 @@ 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")
|
@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 +45,12 @@ 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");
|
||||||
|
|
||||||
|
String outputFilename =
|
||||||
|
originalFilename.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ ".pdf"; // Remove file extension and append .pdf
|
||||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,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.converters.UrlToPdfRequest;
|
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
@@ -25,52 +26,52 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@RequestMapping("/api/v1/convert")
|
@RequestMapping("/api/v1/convert")
|
||||||
public class ConvertWebsiteToPDF {
|
public class ConvertWebsiteToPDF {
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a URL to a PDF",
|
summary = "Convert a URL to a PDF",
|
||||||
description = "This endpoint fetches content from a URL and converts it to a PDF format."
|
description =
|
||||||
)
|
"This endpoint fetches content from a URL and converts it to a PDF format. Input:N/A Output:PDF Type:SISO")
|
||||||
public ResponseEntity<byte[]> urlToPdf(@ModelAttribute UrlToPdfRequest request) throws IOException, InterruptedException {
|
public ResponseEntity<byte[]> urlToPdf(@ModelAttribute UrlToPdfRequest request)
|
||||||
String URL = request.getUrlInput();
|
throws IOException, InterruptedException {
|
||||||
|
String URL = request.getUrlInput();
|
||||||
|
|
||||||
// Validate the URL format
|
// Validate the URL format
|
||||||
if(!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
|
if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
|
||||||
throw new IllegalArgumentException("Invalid URL format provided.");
|
throw new IllegalArgumentException("Invalid URL format provided.");
|
||||||
}
|
}
|
||||||
Path tempOutputFile = null;
|
Path tempOutputFile = null;
|
||||||
byte[] pdfBytes;
|
byte[] pdfBytes;
|
||||||
try {
|
try {
|
||||||
// Prepare the output file path
|
// Prepare the output file path
|
||||||
tempOutputFile = Files.createTempFile("output_", ".pdf");
|
tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||||
|
|
||||||
// Prepare the OCRmyPDF command
|
|
||||||
List<String> command = new ArrayList<>();
|
|
||||||
command.add("weasyprint");
|
|
||||||
command.add(URL);
|
|
||||||
command.add(tempOutputFile.toString());
|
|
||||||
|
|
||||||
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT).runCommandWithOutputHandling(command);
|
|
||||||
|
|
||||||
// Read the optimized PDF file
|
|
||||||
pdfBytes = Files.readAllBytes(tempOutputFile);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
// Clean up the temporary files
|
|
||||||
Files.delete(tempOutputFile);
|
|
||||||
}
|
|
||||||
// Convert URL to a safe filename
|
|
||||||
String outputFilename = convertURLToFileName(URL);
|
|
||||||
|
|
||||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String convertURLToFileName(String url) {
|
// Prepare the OCRmyPDF command
|
||||||
String safeName = url.replaceAll("[^a-zA-Z0-9]", "_");
|
List<String> command = new ArrayList<>();
|
||||||
if(safeName.length() > 50) {
|
command.add("weasyprint");
|
||||||
safeName = safeName.substring(0, 50); // restrict to 50 characters
|
command.add(URL);
|
||||||
}
|
command.add(tempOutputFile.toString());
|
||||||
return safeName + ".pdf";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
ProcessExecutorResult returnCode =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
|
||||||
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
|
// Read the optimized PDF file
|
||||||
|
pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||||
|
} finally {
|
||||||
|
// Clean up the temporary files
|
||||||
|
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";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
import com.opencsv.CSVWriter;
|
import java.io.ByteArrayInputStream;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import java.io.StringWriter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import java.util.ArrayList;
|
||||||
|
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.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -11,41 +13,46 @@ import org.springframework.http.ContentDisposition;
|
|||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
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.*;
|
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 com.opencsv.CSVWriter;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
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;
|
||||||
|
|
||||||
import java.awt.*;
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.StringWriter;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/convert")
|
@RequestMapping("/api/v1/convert")
|
||||||
@Tag(name = "General", description = "General APIs")
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
public class ExtractController {
|
public class ExtractController {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
|
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
|
||||||
|
|
||||||
@PostMapping(value = "/pdf-to-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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,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]));
|
||||||
@@ -82,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]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.PDFComparisonAndCount;
|
import stirling.software.SPDF.model.api.PDFComparisonAndCount;
|
||||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
import stirling.software.SPDF.model.api.filter.ContainsTextRequest;
|
import stirling.software.SPDF.model.api.filter.ContainsTextRequest;
|
||||||
@@ -28,169 +29,182 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@Tag(name = "Filter", description = "Filter APIs")
|
@Tag(name = "Filter", description = "Filter APIs")
|
||||||
public class FilterController {
|
public class FilterController {
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text")
|
@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")
|
@Operation(
|
||||||
public ResponseEntity<byte[]> containsText(@ModelAttribute ContainsTextRequest request) throws IOException, InterruptedException {
|
summary = "Checks if a PDF contains set text, returns true if does",
|
||||||
MultipartFile inputFile = request.getFileInput();
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
String text = request.getText();
|
public ResponseEntity<byte[]> containsText(@ModelAttribute ContainsTextRequest request)
|
||||||
String pageNumber = request.getPageNumbers();
|
throws IOException, InterruptedException {
|
||||||
|
MultipartFile inputFile = request.getFileInput();
|
||||||
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
|
String text = request.getText();
|
||||||
if (PdfUtils.hasText(pdfDocument, pageNumber, text))
|
String pageNumber = request.getPageNumbers();
|
||||||
return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image")
|
if (PdfUtils.hasText(pdfDocument, pageNumber, text))
|
||||||
@Operation(summary = "Checks if a PDF contains an image", description = "Input:PDF Output:Boolean Type:SISO")
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
public ResponseEntity<byte[]> containsImage(@ModelAttribute PDFWithPageNums request)
|
pdfDocument, inputFile.getOriginalFilename());
|
||||||
throws IOException, InterruptedException {
|
return null;
|
||||||
MultipartFile inputFile = request.getFileInput();
|
}
|
||||||
String pageNumber = request.getPageNumbers();
|
|
||||||
|
|
||||||
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
|
|
||||||
if (PdfUtils.hasImages(pdfDocument, pageNumber))
|
|
||||||
return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-count")
|
// TODO
|
||||||
@Operation(summary = "Checks if a PDF is greater, less or equal to a setPageCount", description = "Input:PDF Output:Boolean Type:SISO")
|
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image")
|
||||||
public ResponseEntity<byte[]> pageCount(@ModelAttribute PDFComparisonAndCount request) throws IOException, InterruptedException {
|
@Operation(
|
||||||
MultipartFile inputFile = request.getFileInput();
|
summary = "Checks if a PDF contains an image",
|
||||||
String pageCount = request.getPageCount();
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
String comparator = request.getComparator();
|
public ResponseEntity<byte[]> containsImage(@ModelAttribute PDFWithPageNums request)
|
||||||
// Load the PDF
|
throws IOException, InterruptedException {
|
||||||
PDDocument document = PDDocument.load(inputFile.getInputStream());
|
MultipartFile inputFile = request.getFileInput();
|
||||||
int actualPageCount = document.getNumberOfPages();
|
String pageNumber = request.getPageNumbers();
|
||||||
|
|
||||||
boolean valid = false;
|
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
|
||||||
// Perform the comparison
|
if (PdfUtils.hasImages(pdfDocument, pageNumber))
|
||||||
switch (comparator) {
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
case "Greater":
|
pdfDocument, inputFile.getOriginalFilename());
|
||||||
valid = actualPageCount > Integer.parseInt(pageCount);
|
return null;
|
||||||
break;
|
}
|
||||||
case "Equal":
|
|
||||||
valid = actualPageCount == Integer.parseInt(pageCount);
|
|
||||||
break;
|
|
||||||
case "Less":
|
|
||||||
valid = actualPageCount < Integer.parseInt(pageCount);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valid)
|
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-count")
|
||||||
return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
@Operation(
|
||||||
return null;
|
summary = "Checks if a PDF is greater, less or equal to a setPageCount",
|
||||||
}
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> pageCount(@ModelAttribute PDFComparisonAndCount request)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
MultipartFile inputFile = request.getFileInput();
|
||||||
|
String pageCount = request.getPageCount();
|
||||||
|
String comparator = request.getComparator();
|
||||||
|
// Load the PDF
|
||||||
|
PDDocument document = PDDocument.load(inputFile.getInputStream());
|
||||||
|
int actualPageCount = document.getNumberOfPages();
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-size")
|
boolean valid = false;
|
||||||
@Operation(summary = "Checks if a PDF is of a certain size", description = "Input:PDF Output:Boolean Type:SISO")
|
// Perform the comparison
|
||||||
public ResponseEntity<byte[]> pageSize(@ModelAttribute PageSizeRequest request) throws IOException, InterruptedException {
|
switch (comparator) {
|
||||||
MultipartFile inputFile = request.getFileInput();
|
case "Greater":
|
||||||
String standardPageSize = request.getStandardPageSize();
|
valid = actualPageCount > Integer.parseInt(pageCount);
|
||||||
String comparator = request.getComparator();
|
break;
|
||||||
|
case "Equal":
|
||||||
|
valid = actualPageCount == Integer.parseInt(pageCount);
|
||||||
|
break;
|
||||||
|
case "Less":
|
||||||
|
valid = actualPageCount < Integer.parseInt(pageCount);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
||||||
|
}
|
||||||
|
|
||||||
// Load the PDF
|
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||||
PDDocument document = PDDocument.load(inputFile.getInputStream());
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
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",
|
||||||
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> pageSize(@ModelAttribute PageSizeRequest request)
|
||||||
|
throws IOException, InterruptedException {
|
||||||
|
MultipartFile inputFile = request.getFileInput();
|
||||||
|
String standardPageSize = request.getStandardPageSize();
|
||||||
|
String comparator = request.getComparator();
|
||||||
|
|
||||||
// Calculate the area of the actual page size
|
// Load the PDF
|
||||||
float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight();
|
PDDocument document = PDDocument.load(inputFile.getInputStream());
|
||||||
|
|
||||||
// Get the standard size and calculate its area
|
PDPage firstPage = document.getPage(0);
|
||||||
PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize);
|
PDRectangle actualPageSize = firstPage.getMediaBox();
|
||||||
float standardArea = standardSize.getWidth() * standardSize.getHeight();
|
|
||||||
|
|
||||||
boolean valid = false;
|
// Calculate the area of the actual page size
|
||||||
// Perform the comparison
|
float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight();
|
||||||
switch (comparator) {
|
|
||||||
case "Greater":
|
|
||||||
valid = actualArea > standardArea;
|
|
||||||
break;
|
|
||||||
case "Equal":
|
|
||||||
valid = actualArea == standardArea;
|
|
||||||
break;
|
|
||||||
case "Less":
|
|
||||||
valid = actualArea < standardArea;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valid)
|
// Get the standard size and calculate its area
|
||||||
return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize);
|
||||||
return null;
|
float standardArea = standardSize.getWidth() * standardSize.getHeight();
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-file-size")
|
boolean valid = false;
|
||||||
@Operation(summary = "Checks if a PDF is a set file size", description = "Input:PDF Output:Boolean Type:SISO")
|
// Perform the comparison
|
||||||
public ResponseEntity<byte[]> fileSize(@ModelAttribute FileSizeRequest request) throws IOException, InterruptedException {
|
switch (comparator) {
|
||||||
MultipartFile inputFile = request.getFileInput();
|
case "Greater":
|
||||||
String fileSize = request.getFileSize();
|
valid = actualArea > standardArea;
|
||||||
String comparator = request.getComparator();
|
break;
|
||||||
|
case "Equal":
|
||||||
|
valid = actualArea == standardArea;
|
||||||
|
break;
|
||||||
|
case "Less":
|
||||||
|
valid = actualArea < standardArea;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
||||||
|
}
|
||||||
|
|
||||||
// Get the file size
|
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||||
long actualFileSize = inputFile.getSize();
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
boolean valid = false;
|
@PostMapping(consumes = "multipart/form-data", value = "/filter-file-size")
|
||||||
// Perform the comparison
|
@Operation(
|
||||||
switch (comparator) {
|
summary = "Checks if a PDF is a set file size",
|
||||||
case "Greater":
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
valid = actualFileSize > Long.parseLong(fileSize);
|
public ResponseEntity<byte[]> fileSize(@ModelAttribute FileSizeRequest request)
|
||||||
break;
|
throws IOException, InterruptedException {
|
||||||
case "Equal":
|
MultipartFile inputFile = request.getFileInput();
|
||||||
valid = actualFileSize == Long.parseLong(fileSize);
|
String fileSize = request.getFileSize();
|
||||||
break;
|
String comparator = request.getComparator();
|
||||||
case "Less":
|
|
||||||
valid = actualFileSize < Long.parseLong(fileSize);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valid)
|
// Get the file size
|
||||||
return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
long actualFileSize = inputFile.getSize();
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation")
|
boolean valid = false;
|
||||||
@Operation(summary = "Checks if a PDF is of a certain rotation", description = "Input:PDF Output:Boolean Type:SISO")
|
// Perform the comparison
|
||||||
public ResponseEntity<byte[]> pageRotation(@ModelAttribute PageRotationRequest request) throws IOException, InterruptedException {
|
switch (comparator) {
|
||||||
MultipartFile inputFile = request.getFileInput();
|
case "Greater":
|
||||||
int rotation = request.getRotation();
|
valid = actualFileSize > Long.parseLong(fileSize);
|
||||||
String comparator = request.getComparator();
|
break;
|
||||||
|
case "Equal":
|
||||||
|
valid = actualFileSize == Long.parseLong(fileSize);
|
||||||
|
break;
|
||||||
|
case "Less":
|
||||||
|
valid = actualFileSize < Long.parseLong(fileSize);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
||||||
|
}
|
||||||
|
|
||||||
// Load the PDF
|
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||||
PDDocument document = PDDocument.load(inputFile.getInputStream());
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Get the rotation of the first page
|
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation")
|
||||||
PDPage firstPage = document.getPage(0);
|
@Operation(
|
||||||
int actualRotation = firstPage.getRotation();
|
summary = "Checks if a PDF is of a certain rotation",
|
||||||
boolean valid = false;
|
description = "Input:PDF Output:Boolean Type:SISO")
|
||||||
// Perform the comparison
|
public ResponseEntity<byte[]> pageRotation(@ModelAttribute PageRotationRequest request)
|
||||||
switch (comparator) {
|
throws IOException, InterruptedException {
|
||||||
case "Greater":
|
MultipartFile inputFile = request.getFileInput();
|
||||||
valid = actualRotation > rotation;
|
int rotation = request.getRotation();
|
||||||
break;
|
String comparator = request.getComparator();
|
||||||
case "Equal":
|
|
||||||
valid = actualRotation == rotation;
|
|
||||||
break;
|
|
||||||
case "Less":
|
|
||||||
valid = actualRotation < rotation;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valid)
|
// Load the PDF
|
||||||
return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
PDDocument document = PDDocument.load(inputFile.getInputStream());
|
||||||
return null;
|
|
||||||
|
|
||||||
}
|
// Get the rotation of the first page
|
||||||
|
PDPage firstPage = document.getPage(0);
|
||||||
|
int actualRotation = firstPage.getRotation();
|
||||||
|
boolean valid = false;
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -43,8 +45,12 @@ public class AutoSplitPdfController {
|
|||||||
private static final String QR_CONTENT = "https://github.com/Frooodle/Stirling-PDF";
|
private static final String QR_CONTENT = "https://github.com/Frooodle/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 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));
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
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;
|
||||||
import java.awt.image.RenderedImage;
|
import java.awt.image.RenderedImage;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.zip.Deflater;
|
import java.util.zip.Deflater;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
@@ -31,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")
|
||||||
@@ -41,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
|
||||||
@@ -71,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);
|
||||||
|
|
||||||
@@ -113,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -21,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.misc.AddPageNumbersRequest;
|
import stirling.software.SPDF.model.api.misc.AddPageNumbersRequest;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
@@ -33,16 +34,20 @@ 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")
|
@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")
|
@Operation(
|
||||||
public ResponseEntity<byte[]> addPageNumbers(@ModelAttribute AddPageNumbersRequest request) throws IOException {
|
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")
|
||||||
|
public ResponseEntity<byte[]> addPageNumbers(@ModelAttribute AddPageNumbersRequest request)
|
||||||
|
throws IOException {
|
||||||
MultipartFile file = request.getFileInput();
|
MultipartFile file = request.getFileInput();
|
||||||
String customMargin = request.getCustomMargin();
|
String customMargin = request.getCustomMargin();
|
||||||
int position = request.getPosition();
|
int position = request.getPosition();
|
||||||
int startingNumber = request.getStartingNumber();
|
int startingNumber = request.getStartingNumber();
|
||||||
String pagesToNumber = request.getPagesToNumber();
|
String pagesToNumber = request.getPagesToNumber();
|
||||||
String customText = request.getCustomText();
|
String customText = request.getCustomText();
|
||||||
int pageNumber = startingNumber;
|
int pageNumber = startingNumber;
|
||||||
byte[] fileBytes = file.getBytes();
|
byte[] fileBytes = file.getBytes();
|
||||||
PDDocument document = PDDocument.load(fileBytes);
|
PDDocument document = PDDocument.load(fileBytes);
|
||||||
|
|
||||||
float marginFactor;
|
float marginFactor;
|
||||||
@@ -58,9 +63,8 @@ public class PageNumbersController {
|
|||||||
break;
|
break;
|
||||||
case "x-large":
|
case "x-large":
|
||||||
marginFactor = 0.075f;
|
marginFactor = 0.075f;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
marginFactor = 0.035f;
|
marginFactor = 0.035f;
|
||||||
break;
|
break;
|
||||||
@@ -68,19 +72,29 @@ public class PageNumbersController {
|
|||||||
|
|
||||||
float fontSize = 12.0f;
|
float fontSize = 12.0f;
|
||||||
PDType1Font font = PDType1Font.HELVETICA;
|
PDType1Font font = PDType1Font.HELVETICA;
|
||||||
if(pagesToNumber == null || pagesToNumber.length() == 0) {
|
if (pagesToNumber == null || pagesToNumber.length() == 0) {
|
||||||
pagesToNumber = "all";
|
pagesToNumber = "all";
|
||||||
}
|
}
|
||||||
if(customText == null || customText.length() == 0) {
|
if (customText == null || customText.length() == 0) {
|
||||||
customText = "{n}";
|
customText = "{n}";
|
||||||
}
|
}
|
||||||
List<Integer> pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages());
|
List<Integer> pagesToNumberList =
|
||||||
|
GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages());
|
||||||
|
|
||||||
for (int i : pagesToNumberList) {
|
for (int i : pagesToNumberList) {
|
||||||
PDPage page = document.getPage(i);
|
PDPage page = document.getPage(i);
|
||||||
PDRectangle pageSize = page.getMediaBox();
|
PDRectangle pageSize = page.getMediaBox();
|
||||||
|
|
||||||
String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(document.getNumberOfPages())).replace("{filename}", file.getOriginalFilename().replaceFirst("[.][^.]+$", "")) : String.valueOf(pageNumber);
|
String text =
|
||||||
|
customText != null
|
||||||
|
? customText
|
||||||
|
.replace("{n}", String.valueOf(pageNumber))
|
||||||
|
.replace("{total}", String.valueOf(document.getNumberOfPages()))
|
||||||
|
.replace(
|
||||||
|
"{filename}",
|
||||||
|
file.getOriginalFilename()
|
||||||
|
.replaceFirst("[.][^.]+$", ""))
|
||||||
|
: String.valueOf(pageNumber);
|
||||||
|
|
||||||
float x, y;
|
float x, y;
|
||||||
|
|
||||||
@@ -88,10 +102,10 @@ public class PageNumbersController {
|
|||||||
int yGroup = 2 - (position - 1) / 3;
|
int yGroup = 2 - (position - 1) / 3;
|
||||||
|
|
||||||
switch (xGroup) {
|
switch (xGroup) {
|
||||||
case 0: // left
|
case 0: // left
|
||||||
x = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth();
|
x = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth();
|
||||||
break;
|
break;
|
||||||
case 1: // center
|
case 1: // center
|
||||||
x = pageSize.getLowerLeftX() + (pageSize.getWidth() / 2);
|
x = pageSize.getLowerLeftX() + (pageSize.getWidth() / 2);
|
||||||
break;
|
break;
|
||||||
default: // right
|
default: // right
|
||||||
@@ -100,10 +114,10 @@ public class PageNumbersController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (yGroup) {
|
switch (yGroup) {
|
||||||
case 0: // bottom
|
case 0: // bottom
|
||||||
y = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight();
|
y = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight();
|
||||||
break;
|
break;
|
||||||
case 1: // middle
|
case 1: // middle
|
||||||
y = pageSize.getLowerLeftY() + (pageSize.getHeight() / 2);
|
y = pageSize.getLowerLeftY() + (pageSize.getHeight() / 2);
|
||||||
break;
|
break;
|
||||||
default: // top
|
default: // top
|
||||||
@@ -111,7 +125,9 @@ public class PageNumbersController {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true);
|
PDPageContentStream contentStream =
|
||||||
|
new PDPageContentStream(
|
||||||
|
document, page, PDPageContentStream.AppendMode.APPEND, true);
|
||||||
contentStream.beginText();
|
contentStream.beginText();
|
||||||
contentStream.setFont(font, fontSize);
|
contentStream.setFont(font, fontSize);
|
||||||
contentStream.newLineAtOffset(x, y);
|
contentStream.newLineAtOffset(x, y);
|
||||||
@@ -126,10 +142,9 @@ public class PageNumbersController {
|
|||||||
document.save(baos);
|
document.save(baos);
|
||||||
document.close();
|
document.close();
|
||||||
|
|
||||||
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", MediaType.APPLICATION_PDF);
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
baos.toByteArray(),
|
||||||
|
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf",
|
||||||
|
MediaType.APPLICATION_PDF);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,47 +15,62 @@ 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 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")
|
||||||
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.pipeline;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletContext;
|
||||||
|
import stirling.software.SPDF.SPdfApplication;
|
||||||
|
import stirling.software.SPDF.model.ApiEndpoint;
|
||||||
|
import stirling.software.SPDF.model.Role;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class ApiDocService {
|
||||||
|
|
||||||
|
private final Map<String, ApiEndpoint> apiDocumentation = new HashMap<>();
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ApiDocService.class);
|
||||||
|
|
||||||
|
@Autowired private ServletContext servletContext;
|
||||||
|
|
||||||
|
private String getApiDocsUrl() {
|
||||||
|
String contextPath = servletContext.getContextPath();
|
||||||
|
String port = SPdfApplication.getPort();
|
||||||
|
|
||||||
|
return "http://localhost:" + port + contextPath + "/v1/api-docs";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private UserServiceInterface userService;
|
||||||
|
|
||||||
|
private String getApiKeyForUser() {
|
||||||
|
if (userService == null) return "";
|
||||||
|
return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId());
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode apiDocsJsonRootNode;
|
||||||
|
|
||||||
|
// @EventListener(ApplicationReadyEvent.class)
|
||||||
|
private synchronized void loadApiDocumentation() {
|
||||||
|
String apiDocsJson = "";
|
||||||
|
try {
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
String apiKey = getApiKeyForUser();
|
||||||
|
if (!apiKey.isEmpty()) {
|
||||||
|
headers.set("X-API-KEY", apiKey);
|
||||||
|
}
|
||||||
|
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||||
|
|
||||||
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
|
ResponseEntity<String> response =
|
||||||
|
restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class);
|
||||||
|
apiDocsJson = response.getBody();
|
||||||
|
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
apiDocsJsonRootNode = mapper.readTree(apiDocsJson);
|
||||||
|
|
||||||
|
JsonNode paths = apiDocsJsonRootNode.path("paths");
|
||||||
|
paths.fields()
|
||||||
|
.forEachRemaining(
|
||||||
|
entry -> {
|
||||||
|
String path = entry.getKey();
|
||||||
|
JsonNode pathNode = entry.getValue();
|
||||||
|
if (pathNode.has("post")) {
|
||||||
|
JsonNode postNode = pathNode.get("post");
|
||||||
|
ApiEndpoint endpoint = new ApiEndpoint(path, postNode);
|
||||||
|
apiDocumentation.put(path, endpoint);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Handle exceptions
|
||||||
|
logger.error("Error grabbing swagger doc, body result {}", apiDocsJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValidOperation(String operationName, Map<String, Object> parameters) {
|
||||||
|
if (apiDocumentation.size() == 0) {
|
||||||
|
loadApiDocumentation();
|
||||||
|
}
|
||||||
|
if (!apiDocumentation.containsKey(operationName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ApiEndpoint endpoint = apiDocumentation.get(operationName);
|
||||||
|
return endpoint.areParametersValid(parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMultiInput(String operationName) {
|
||||||
|
if (apiDocsJsonRootNode == null || apiDocumentation.size() == 0) {
|
||||||
|
loadApiDocumentation();
|
||||||
|
}
|
||||||
|
if (!apiDocumentation.containsKey(operationName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiEndpoint endpoint = apiDocumentation.get(operationName);
|
||||||
|
String description = endpoint.getDescription();
|
||||||
|
|
||||||
|
Pattern pattern = Pattern.compile("Type:(\\w+)");
|
||||||
|
Matcher matcher = pattern.matcher(description);
|
||||||
|
if (matcher.find()) {
|
||||||
|
String type = matcher.group(1);
|
||||||
|
return type.startsWith("MI");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model class for API Endpoint
|
||||||
@@ -1,56 +1,32 @@
|
|||||||
package stirling.software.SPDF.controller.api.pipeline;
|
package stirling.software.SPDF.controller.api.pipeline;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.PrintStream;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
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.core.io.ByteArrayResource;
|
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.HttpEntity;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpMethod;
|
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
|
||||||
import org.springframework.util.LinkedMultiValueMap;
|
|
||||||
import org.springframework.util.MultiValueMap;
|
|
||||||
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.client.RestTemplate;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.PipelineConfig;
|
import stirling.software.SPDF.model.PipelineConfig;
|
||||||
import stirling.software.SPDF.model.PipelineOperation;
|
|
||||||
import stirling.software.SPDF.model.api.HandleDataRequest;
|
import stirling.software.SPDF.model.api.HandleDataRequest;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@@ -59,466 +35,80 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@Tag(name = "Pipeline", description = "Pipeline APIs")
|
@Tag(name = "Pipeline", description = "Pipeline APIs")
|
||||||
public class PipelineController {
|
public class PipelineController {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(PipelineController.class);
|
private static final Logger logger = LoggerFactory.getLogger(PipelineController.class);
|
||||||
@Autowired
|
|
||||||
private ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
final String jsonFileName = "pipelineConfig.json";
|
final String watchedFoldersDir = "./pipeline/watchedFolders/";
|
||||||
final String watchedFoldersDir = "./pipeline/watchedFolders/";
|
final String finishedFoldersDir = "./pipeline/finishedFolders/";
|
||||||
final String finishedFoldersDir = "./pipeline/finishedFolders/";
|
@Autowired PipelineProcessor processor;
|
||||||
|
|
||||||
@Scheduled(fixedRate = 25000)
|
|
||||||
public void scanFolders() {
|
|
||||||
logger.info("Scanning folders...");
|
|
||||||
Path watchedFolderPath = Paths.get(watchedFoldersDir);
|
|
||||||
if (!Files.exists(watchedFolderPath)) {
|
|
||||||
try {
|
|
||||||
Files.createDirectories(watchedFolderPath);
|
|
||||||
logger.info("Created directory: {}", watchedFolderPath);
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.error("Error creating directory: {}", watchedFolderPath, e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try (Stream<Path> paths = Files.walk(watchedFolderPath)) {
|
|
||||||
paths.filter(Files::isDirectory).forEach(t -> {
|
|
||||||
try {
|
|
||||||
if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) {
|
|
||||||
handleDirectory(t);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("Error handling directory: {}", t, e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("Error walking through directory: {}", watchedFolderPath, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
|
|
||||||
private void handleDirectory(Path dir) throws Exception {
|
@Autowired private ObjectMapper objectMapper;
|
||||||
logger.info("Handling directory: {}", dir);
|
|
||||||
Path jsonFile = dir.resolve(jsonFileName);
|
|
||||||
Path processingDir = dir.resolve("processing"); // Directory to move files during processing
|
|
||||||
if (!Files.exists(processingDir)) {
|
|
||||||
Files.createDirectory(processingDir);
|
|
||||||
logger.info("Created processing directory: {}", processingDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Files.exists(jsonFile)) {
|
@PostMapping("/handleData")
|
||||||
// Read JSON file
|
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request)
|
||||||
String jsonString;
|
throws JsonMappingException, JsonProcessingException {
|
||||||
try {
|
if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) {
|
||||||
jsonString = new String(Files.readAllBytes(jsonFile));
|
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||||
logger.info("Read JSON file: {}", jsonFile);
|
}
|
||||||
} catch (IOException e) {
|
|
||||||
logger.error("Error reading JSON file: {}", jsonFile, e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode JSON to PipelineConfig
|
MultipartFile[] files = request.getFileInput();
|
||||||
PipelineConfig config;
|
String jsonString = request.getJson();
|
||||||
try {
|
if (files == null) {
|
||||||
config = objectMapper.readValue(jsonString, PipelineConfig.class);
|
return null;
|
||||||
// Assuming your PipelineConfig class has getters for all necessary fields, you
|
}
|
||||||
// can perform checks here
|
PipelineConfig config = objectMapper.readValue(jsonString, PipelineConfig.class);
|
||||||
if (config.getOperations() == null || config.getOutputDir() == null || config.getName() == null) {
|
logger.info("Received POST request to /handleData with {} files", files.length);
|
||||||
throw new IOException("Invalid JSON format");
|
try {
|
||||||
}
|
List<Resource> inputFiles = processor.generateInputFiles(files);
|
||||||
} catch (IOException e) {
|
if (inputFiles == null || inputFiles.size() == 0) {
|
||||||
logger.error("Error parsing PipelineConfig: {}", jsonString, e);
|
return null;
|
||||||
return;
|
}
|
||||||
}
|
List<Resource> outputFiles = processor.runPipelineAgainstFiles(inputFiles, config);
|
||||||
|
if (outputFiles != null && outputFiles.size() == 1) {
|
||||||
|
// If there is only one file, return it directly
|
||||||
|
Resource singleFile = outputFiles.get(0);
|
||||||
|
InputStream is = singleFile.getInputStream();
|
||||||
|
byte[] bytes = new byte[(int) singleFile.contentLength()];
|
||||||
|
is.read(bytes);
|
||||||
|
is.close();
|
||||||
|
|
||||||
// For each operation in the pipeline
|
logger.info("Returning single file response...");
|
||||||
for (PipelineOperation operation : config.getOperations()) {
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
// Collect all files based on fileInput
|
bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM);
|
||||||
File[] files;
|
} else if (outputFiles == null) {
|
||||||
String fileInput = (String) operation.getParameters().get("fileInput");
|
return null;
|
||||||
if ("automated".equals(fileInput)) {
|
}
|
||||||
// If fileInput is "automated", process all files in the directory
|
|
||||||
try (Stream<Path> paths = Files.list(dir)) {
|
|
||||||
files = paths
|
|
||||||
.filter(path -> !Files.isDirectory(path)) // exclude directories
|
|
||||||
.filter(path -> !path.equals(jsonFile)) // exclude jsonFile
|
|
||||||
.map(Path::toFile)
|
|
||||||
.toArray(File[]::new);
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
// Create a ByteArrayOutputStream to hold the zip
|
||||||
e.printStackTrace();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
return;
|
ZipOutputStream zipOut = new ZipOutputStream(baos);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If fileInput contains a path, process only this file
|
|
||||||
files = new File[] { new File(fileInput) };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare the files for processing
|
// Loop through each file and add it to the zip
|
||||||
List<File> filesToProcess = new ArrayList<>();
|
for (Resource file : outputFiles) {
|
||||||
for (File file : files) {
|
ZipEntry zipEntry = new ZipEntry(file.getFilename());
|
||||||
logger.info(file.getName());
|
zipOut.putNextEntry(zipEntry);
|
||||||
logger.info("{} to {}",file.toPath(), processingDir.resolve(file.getName()));
|
|
||||||
Files.move(file.toPath(), processingDir.resolve(file.getName()));
|
|
||||||
filesToProcess.add(processingDir.resolve(file.getName()).toFile());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process the files
|
// Read the file into a byte array
|
||||||
try {
|
InputStream is = file.getInputStream();
|
||||||
List<Resource> resources = handleFiles(filesToProcess.toArray(new File[0]), jsonString);
|
byte[] bytes = new byte[(int) file.contentLength()];
|
||||||
|
is.read(bytes);
|
||||||
if(resources == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Move resultant files and rename them as per config in JSON file
|
|
||||||
for (Resource resource : resources) {
|
|
||||||
String resourceName = resource.getFilename();
|
|
||||||
String baseName = resourceName.substring(0, resourceName.lastIndexOf("."));
|
|
||||||
String extension = resourceName.substring(resourceName.lastIndexOf(".")+1);
|
|
||||||
|
|
||||||
String outputFileName = config.getOutputPattern().replace("{filename}", baseName);
|
|
||||||
|
|
||||||
outputFileName = outputFileName.replace("{pipelineName}", config.getName());
|
|
||||||
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
|
|
||||||
outputFileName = outputFileName.replace("{date}", LocalDate.now().format(dateFormatter));
|
|
||||||
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HHmmss");
|
|
||||||
outputFileName = outputFileName.replace("{time}", LocalTime.now().format(timeFormatter));
|
|
||||||
|
|
||||||
outputFileName += "." + extension;
|
|
||||||
// {filename} {folder} {date} {tmime} {pipeline}
|
|
||||||
String outputDir = config.getOutputDir();
|
|
||||||
|
|
||||||
String outputFolder = applicationProperties.getAutoPipeline().getOutputFolder();
|
// Write the bytes of the file to the zip
|
||||||
|
zipOut.write(bytes, 0, bytes.length);
|
||||||
|
zipOut.closeEntry();
|
||||||
|
|
||||||
if (outputFolder == null || outputFolder.isEmpty()) {
|
is.close();
|
||||||
// If the environment variable is not set, use the default value
|
}
|
||||||
outputFolder = finishedFoldersDir;
|
|
||||||
}
|
|
||||||
logger.info("outputDir 0={}", outputDir);
|
|
||||||
// Replace the placeholders in the outputDir string
|
|
||||||
outputDir = outputDir.replace("{outputFolder}", outputFolder);
|
|
||||||
outputDir = outputDir.replace("{folderName}", dir.toString());
|
|
||||||
logger.info("outputDir 1={}", outputDir);
|
|
||||||
outputDir = outputDir.replace("\\watchedFolders", "");
|
|
||||||
outputDir = outputDir.replace("//watchedFolders", "");
|
|
||||||
outputDir = outputDir.replace("\\\\watchedFolders", "");
|
|
||||||
outputDir = outputDir.replace("/watchedFolders", "");
|
|
||||||
|
|
||||||
Path outputPath;
|
|
||||||
logger.info("outputDir 2={}", outputDir);
|
|
||||||
if (Paths.get(outputDir).isAbsolute()) {
|
|
||||||
// If it's an absolute path, use it directly
|
|
||||||
outputPath = Paths.get(outputDir);
|
|
||||||
} else {
|
|
||||||
// If it's a relative path, make it relative to the current working directory
|
|
||||||
outputPath = Paths.get(".", outputDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("outputPath={}", outputPath);
|
|
||||||
|
|
||||||
if (!Files.exists(outputPath)) {
|
|
||||||
try {
|
|
||||||
Files.createDirectories(outputPath);
|
|
||||||
logger.info("Created directory: {}", outputPath);
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.error("Error creating directory: {}", outputPath, e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.info("outputPath {}", outputPath);
|
|
||||||
logger.info("outputPath.resolve(outputFileName).toString() {}", outputPath.resolve(outputFileName).toString());
|
|
||||||
File newFile = new File(outputPath.resolve(outputFileName).toString());
|
|
||||||
OutputStream os = new FileOutputStream(newFile);
|
|
||||||
os.write(((ByteArrayResource)resource).getByteArray());
|
|
||||||
os.close();
|
|
||||||
logger.info("made {}", outputPath.resolve(outputFileName));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If successful, delete the original files
|
zipOut.close();
|
||||||
for (File file : filesToProcess) {
|
|
||||||
Files.deleteIfExists(processingDir.resolve(file.getName()));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// If an error occurs, move the original files back
|
|
||||||
for (File file : filesToProcess) {
|
|
||||||
Files.move(processingDir.resolve(file.getName()), file.toPath());
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Resource> processFiles(List<Resource> outputFiles, String jsonString) throws Exception {
|
|
||||||
|
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
|
||||||
JsonNode jsonNode = mapper.readTree(jsonString);
|
|
||||||
|
|
||||||
JsonNode pipelineNode = jsonNode.get("pipeline");
|
|
||||||
logger.info("Running pipelineNode: {}", pipelineNode);
|
|
||||||
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
|
|
||||||
PrintStream logPrintStream = new PrintStream(logStream);
|
|
||||||
|
|
||||||
boolean hasErrors = false;
|
|
||||||
|
|
||||||
for (JsonNode operationNode : pipelineNode) {
|
|
||||||
String operation = operationNode.get("operation").asText();
|
|
||||||
logger.info("Running operation: {}", operation);
|
|
||||||
JsonNode parametersNode = operationNode.get("parameters");
|
|
||||||
String inputFileExtension = "";
|
|
||||||
if (operationNode.has("inputFileType")) {
|
|
||||||
inputFileExtension = operationNode.get("inputFileType").asText();
|
|
||||||
} else {
|
|
||||||
inputFileExtension = ".pdf";
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Resource> newOutputFiles = new ArrayList<>();
|
|
||||||
boolean hasInputFileType = false;
|
|
||||||
|
|
||||||
for (Resource file : outputFiles) {
|
|
||||||
if (file.getFilename().endsWith(inputFileExtension)) {
|
|
||||||
hasInputFileType = true;
|
|
||||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
|
||||||
body.add("fileInput", file);
|
|
||||||
|
|
||||||
Iterator<Map.Entry<String, JsonNode>> parameters = parametersNode.fields();
|
|
||||||
while (parameters.hasNext()) {
|
|
||||||
Map.Entry<String, JsonNode> parameter = parameters.next();
|
|
||||||
body.add(parameter.getKey(), parameter.getValue().asText());
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
|
||||||
|
|
||||||
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
|
|
||||||
|
|
||||||
RestTemplate restTemplate = new RestTemplate();
|
|
||||||
String url = "http://localhost:8080/" + operation;
|
|
||||||
|
|
||||||
ResponseEntity<byte[]> response = restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
|
|
||||||
|
|
||||||
// If the operation is filter and the response body is null or empty, skip this 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)) {
|
|
||||||
logPrintStream.println("Error: " + response.getBody());
|
|
||||||
hasErrors = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Define filename
|
|
||||||
String filename;
|
|
||||||
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.
|
|
||||||
filename = "file_" + System.currentTimeMillis();
|
|
||||||
} else {
|
|
||||||
// Otherwise, keep the original filename.
|
|
||||||
filename = file.getFilename();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the response body is a zip file
|
|
||||||
if (isZip(response.getBody())) {
|
|
||||||
// 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 filename;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
newOutputFiles.add(outputResource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasInputFileType) {
|
|
||||||
logPrintStream.println(
|
|
||||||
"No files with extension " + inputFileExtension + " found for operation " + operation);
|
|
||||||
hasErrors = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
outputFiles = newOutputFiles;
|
|
||||||
}
|
|
||||||
logPrintStream.close();
|
|
||||||
|
|
||||||
}
|
|
||||||
if (hasErrors) {
|
|
||||||
logger.error("Errors occurred during processing. Log: {}", logStream.toString());
|
|
||||||
}
|
|
||||||
return outputFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Resource> handleFiles(File[] files, String jsonString) throws Exception {
|
|
||||||
if(files == null || files.length == 0) {
|
|
||||||
logger.info("No files");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Handling files: {} files, with JSON string of length: {}", files.length, jsonString.length());
|
|
||||||
|
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
|
||||||
JsonNode jsonNode = mapper.readTree(jsonString);
|
|
||||||
|
|
||||||
JsonNode pipelineNode = jsonNode.get("pipeline");
|
|
||||||
|
|
||||||
boolean hasErrors = false;
|
|
||||||
List<Resource> outputFiles = new ArrayList<>();
|
|
||||||
|
|
||||||
for (File file : files) {
|
|
||||||
Path path = Paths.get(file.getAbsolutePath());
|
|
||||||
System.out.println("Reading file: " + path); // debug statement
|
|
||||||
|
|
||||||
if (Files.exists(path)) {
|
|
||||||
Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) {
|
|
||||||
@Override
|
|
||||||
public String getFilename() {
|
|
||||||
return file.getName();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
outputFiles.add(fileResource);
|
|
||||||
} else {
|
|
||||||
System.out.println("File not found: " + path); // debug statement
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.info("Files successfully loaded. Starting processing...");
|
|
||||||
return processFiles(outputFiles, jsonString);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Resource> handleFiles(MultipartFile[] files, String jsonString) throws Exception {
|
|
||||||
if(files == null || files.length == 0) {
|
|
||||||
logger.info("No files");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
logger.info("Handling files: {} files, with JSON string of length: {}", files.length, jsonString.length());
|
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
|
||||||
JsonNode jsonNode = mapper.readTree(jsonString);
|
|
||||||
|
|
||||||
JsonNode pipelineNode = jsonNode.get("pipeline");
|
|
||||||
|
|
||||||
boolean hasErrors = false;
|
|
||||||
List<Resource> outputFiles = new ArrayList<>();
|
|
||||||
|
|
||||||
for (MultipartFile file : files) {
|
|
||||||
Resource fileResource = new ByteArrayResource(file.getBytes()) {
|
|
||||||
@Override
|
|
||||||
public String getFilename() {
|
|
||||||
return file.getOriginalFilename();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
outputFiles.add(fileResource);
|
|
||||||
}
|
|
||||||
logger.info("Files successfully loaded. Starting processing...");
|
|
||||||
return processFiles(outputFiles, jsonString);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/handleData")
|
|
||||||
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request) {
|
|
||||||
MultipartFile[] files = request.getFileInputs();
|
|
||||||
String jsonString = request.getJsonString();
|
|
||||||
logger.info("Received POST request to /handleData with {} files", files.length);
|
|
||||||
try {
|
|
||||||
List<Resource> outputFiles = handleFiles(files, jsonString);
|
|
||||||
|
|
||||||
if (outputFiles != null && outputFiles.size() == 1) {
|
|
||||||
// If there is only one file, return it directly
|
|
||||||
Resource singleFile = outputFiles.get(0);
|
|
||||||
InputStream is = singleFile.getInputStream();
|
|
||||||
byte[] bytes = new byte[(int) singleFile.contentLength()];
|
|
||||||
is.read(bytes);
|
|
||||||
is.close();
|
|
||||||
|
|
||||||
logger.info("Returning single file response...");
|
|
||||||
return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(),
|
|
||||||
MediaType.APPLICATION_OCTET_STREAM);
|
|
||||||
} else if (outputFiles == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a ByteArrayOutputStream to hold the zip
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
ZipOutputStream zipOut = new ZipOutputStream(baos);
|
|
||||||
|
|
||||||
// Loop through each file and add it to the zip
|
|
||||||
for (Resource file : outputFiles) {
|
|
||||||
ZipEntry zipEntry = new ZipEntry(file.getFilename());
|
|
||||||
zipOut.putNextEntry(zipEntry);
|
|
||||||
|
|
||||||
// Read the file into a byte array
|
|
||||||
InputStream is = file.getInputStream();
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.pipeline;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.PipelineConfig;
|
||||||
|
import stirling.software.SPDF.model.PipelineOperation;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PipelineDirectoryProcessor {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(PipelineDirectoryProcessor.class);
|
||||||
|
@Autowired private ObjectMapper objectMapper;
|
||||||
|
@Autowired private ApiDocService apiDocService;
|
||||||
|
@Autowired private ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
final String watchedFoldersDir = "./pipeline/watchedFolders/";
|
||||||
|
final String finishedFoldersDir = "./pipeline/finishedFolders/";
|
||||||
|
|
||||||
|
@Autowired PipelineProcessor processor;
|
||||||
|
|
||||||
|
@Scheduled(fixedRate = 60000)
|
||||||
|
public void scanFolders() {
|
||||||
|
if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Path watchedFolderPath = Paths.get(watchedFoldersDir);
|
||||||
|
if (!Files.exists(watchedFolderPath)) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(watchedFolderPath);
|
||||||
|
logger.info("Created directory: {}", watchedFolderPath);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Error creating directory: {}", watchedFolderPath, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try (Stream<Path> paths = Files.walk(watchedFolderPath)) {
|
||||||
|
paths.filter(Files::isDirectory)
|
||||||
|
.forEach(
|
||||||
|
t -> {
|
||||||
|
try {
|
||||||
|
if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) {
|
||||||
|
handleDirectory(t);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Error handling directory: {}", t, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Error walking through directory: {}", watchedFolderPath, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleDirectory(Path dir) throws IOException {
|
||||||
|
logger.info("Handling directory: {}", dir);
|
||||||
|
Path processingDir = createProcessingDirectory(dir);
|
||||||
|
|
||||||
|
Optional<Path> jsonFileOptional = findJsonFile(dir);
|
||||||
|
if (!jsonFileOptional.isPresent()) {
|
||||||
|
logger.warn("No .JSON settings file found. No processing will happen for dir {}.", dir);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path jsonFile = jsonFileOptional.get();
|
||||||
|
PipelineConfig config = readAndParseJson(jsonFile);
|
||||||
|
processPipelineOperations(dir, processingDir, jsonFile, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path createProcessingDirectory(Path dir) throws IOException {
|
||||||
|
Path processingDir = dir.resolve("processing");
|
||||||
|
if (!Files.exists(processingDir)) {
|
||||||
|
Files.createDirectory(processingDir);
|
||||||
|
logger.info("Created processing directory: {}", processingDir);
|
||||||
|
}
|
||||||
|
return processingDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Path> findJsonFile(Path dir) throws IOException {
|
||||||
|
try (Stream<Path> paths = Files.list(dir)) {
|
||||||
|
return paths.filter(file -> file.toString().endsWith(".json")).findFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PipelineConfig readAndParseJson(Path jsonFile) throws IOException {
|
||||||
|
String jsonString = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8);
|
||||||
|
logger.debug("Reading JSON file: {}", jsonFile);
|
||||||
|
return objectMapper.readValue(jsonString, PipelineConfig.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processPipelineOperations(
|
||||||
|
Path dir, Path processingDir, Path jsonFile, PipelineConfig config) throws IOException {
|
||||||
|
for (PipelineOperation operation : config.getOperations()) {
|
||||||
|
validateOperation(operation);
|
||||||
|
File[] files = collectFilesForProcessing(dir, jsonFile, operation);
|
||||||
|
if (files == null || files.length == 0) {
|
||||||
|
logger.debug("No files detected for {} ", dir);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<File> filesToProcess = prepareFilesForProcessing(files, processingDir);
|
||||||
|
runPipelineAgainstFiles(filesToProcess, config, dir, processingDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateOperation(PipelineOperation operation) throws IOException {
|
||||||
|
if (!apiDocService.isValidOperation(operation.getOperation(), operation.getParameters())) {
|
||||||
|
throw new IOException("Invalid operation: " + operation.getOperation());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private File[] collectFilesForProcessing(Path dir, Path jsonFile, PipelineOperation operation)
|
||||||
|
throws IOException {
|
||||||
|
try (Stream<Path> paths = Files.list(dir)) {
|
||||||
|
if ("automated".equals(operation.getParameters().get("fileInput"))) {
|
||||||
|
return paths.filter(path -> !Files.isDirectory(path) && !path.equals(jsonFile))
|
||||||
|
.map(Path::toFile)
|
||||||
|
.toArray(File[]::new);
|
||||||
|
} else {
|
||||||
|
String fileInput = (String) operation.getParameters().get("fileInput");
|
||||||
|
return new File[] {new File(fileInput)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<File> prepareFilesForProcessing(File[] files, Path processingDir)
|
||||||
|
throws IOException {
|
||||||
|
List<File> filesToProcess = new ArrayList<>();
|
||||||
|
for (File file : files) {
|
||||||
|
Path targetPath = resolveUniqueFilePath(processingDir, file.getName());
|
||||||
|
Files.move(file.toPath(), targetPath);
|
||||||
|
filesToProcess.add(targetPath.toFile());
|
||||||
|
}
|
||||||
|
return filesToProcess;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path resolveUniqueFilePath(Path directory, String originalFileName) {
|
||||||
|
Path filePath = directory.resolve(originalFileName);
|
||||||
|
int counter = 1;
|
||||||
|
|
||||||
|
while (Files.exists(filePath)) {
|
||||||
|
String newName = appendSuffixToFileName(originalFileName, "(" + counter + ")");
|
||||||
|
filePath = directory.resolve(newName);
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String appendSuffixToFileName(String originalFileName, String suffix) {
|
||||||
|
int dotIndex = originalFileName.lastIndexOf('.');
|
||||||
|
if (dotIndex == -1) {
|
||||||
|
return originalFileName + suffix;
|
||||||
|
} else {
|
||||||
|
return originalFileName.substring(0, dotIndex)
|
||||||
|
+ suffix
|
||||||
|
+ originalFileName.substring(dotIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runPipelineAgainstFiles(
|
||||||
|
List<File> filesToProcess, PipelineConfig config, Path dir, Path processingDir)
|
||||||
|
throws IOException {
|
||||||
|
try {
|
||||||
|
List<Resource> inputFiles =
|
||||||
|
processor.generateInputFiles(filesToProcess.toArray(new File[0]));
|
||||||
|
if (inputFiles == null || inputFiles.size() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Resource> outputFiles = processor.runPipelineAgainstFiles(inputFiles, config);
|
||||||
|
if (outputFiles == null) return;
|
||||||
|
moveAndRenameFiles(outputFiles, config, dir);
|
||||||
|
deleteOriginalFiles(filesToProcess, processingDir);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("error during processing", e);
|
||||||
|
moveFilesBack(filesToProcess, processingDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void moveAndRenameFiles(List<Resource> resources, PipelineConfig config, Path dir)
|
||||||
|
throws IOException {
|
||||||
|
for (Resource resource : resources) {
|
||||||
|
String outputFileName = createOutputFileName(resource, config);
|
||||||
|
Path outputPath = determineOutputPath(config, dir);
|
||||||
|
|
||||||
|
if (!Files.exists(outputPath)) {
|
||||||
|
Files.createDirectories(outputPath);
|
||||||
|
logger.info("Created directory: {}", outputPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path outputFile = outputPath.resolve(outputFileName);
|
||||||
|
try (OutputStream os = new FileOutputStream(outputFile.toFile())) {
|
||||||
|
os.write(((ByteArrayResource) resource).getByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("File moved and renamed to {}", outputFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createOutputFileName(Resource resource, PipelineConfig config) {
|
||||||
|
String resourceName = resource.getFilename();
|
||||||
|
String baseName = resourceName.substring(0, resourceName.lastIndexOf('.'));
|
||||||
|
String extension = resourceName.substring(resourceName.lastIndexOf('.') + 1);
|
||||||
|
|
||||||
|
String outputFileName =
|
||||||
|
config.getOutputPattern()
|
||||||
|
.replace("{filename}", baseName)
|
||||||
|
.replace("{pipelineName}", config.getName())
|
||||||
|
.replace(
|
||||||
|
"{date}",
|
||||||
|
LocalDate.now()
|
||||||
|
.format(DateTimeFormatter.ofPattern("yyyyMMdd")))
|
||||||
|
.replace(
|
||||||
|
"{time}",
|
||||||
|
LocalTime.now()
|
||||||
|
.format(DateTimeFormatter.ofPattern("HHmmss")))
|
||||||
|
+ "."
|
||||||
|
+ extension;
|
||||||
|
|
||||||
|
return outputFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path determineOutputPath(PipelineConfig config, Path dir) {
|
||||||
|
String outputDir =
|
||||||
|
config.getOutputDir()
|
||||||
|
.replace("{outputFolder}", finishedFoldersDir)
|
||||||
|
.replace("{folderName}", dir.toString())
|
||||||
|
.replaceAll("\\\\?watchedFolders", "");
|
||||||
|
|
||||||
|
return Paths.get(outputDir).isAbsolute() ? Paths.get(outputDir) : Paths.get(".", outputDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteOriginalFiles(List<File> filesToProcess, Path processingDir)
|
||||||
|
throws IOException {
|
||||||
|
for (File file : filesToProcess) {
|
||||||
|
Files.deleteIfExists(processingDir.resolve(file.getName()));
|
||||||
|
logger.info("Deleted original file: {}", file.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void moveFilesBack(List<File> filesToProcess, Path processingDir) {
|
||||||
|
for (File file : filesToProcess) {
|
||||||
|
try {
|
||||||
|
Files.move(processingDir.resolve(file.getName()), file.toPath());
|
||||||
|
logger.info(
|
||||||
|
"Moved file back to original location: {} , {}",
|
||||||
|
file.toPath(),
|
||||||
|
file.getName());
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Error moving file back to original location: {}", file.getName(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.pipeline;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletContext;
|
||||||
|
import stirling.software.SPDF.SPdfApplication;
|
||||||
|
import stirling.software.SPDF.model.PipelineConfig;
|
||||||
|
import stirling.software.SPDF.model.PipelineOperation;
|
||||||
|
import stirling.software.SPDF.model.Role;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PipelineProcessor {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(PipelineProcessor.class);
|
||||||
|
|
||||||
|
@Autowired private ApiDocService apiDocService;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private UserServiceInterface userService;
|
||||||
|
|
||||||
|
@Autowired private ServletContext servletContext;
|
||||||
|
|
||||||
|
private String getApiKeyForUser() {
|
||||||
|
if (userService == null) return "";
|
||||||
|
return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getBaseUrl() {
|
||||||
|
String contextPath = servletContext.getContextPath();
|
||||||
|
String port = SPdfApplication.getPort();
|
||||||
|
|
||||||
|
return "http://localhost:" + port + contextPath + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Resource> runPipelineAgainstFiles(List<Resource> outputFiles, PipelineConfig config)
|
||||||
|
throws Exception {
|
||||||
|
|
||||||
|
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
|
||||||
|
PrintStream logPrintStream = new PrintStream(logStream);
|
||||||
|
|
||||||
|
boolean hasErrors = false;
|
||||||
|
|
||||||
|
for (PipelineOperation pipelineOperation : config.getOperations()) {
|
||||||
|
String operation = pipelineOperation.getOperation();
|
||||||
|
boolean isMultiInputOperation = apiDocService.isMultiInput(operation);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Running operation: {} isMultiInputOperation {}",
|
||||||
|
operation,
|
||||||
|
isMultiInputOperation);
|
||||||
|
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);
|
||||||
|
|
||||||
|
for (Entry<String, Object> entry : parameters.entrySet()) {
|
||||||
|
body.add(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
||||||
|
|
||||||
|
// If the operation is filter and the response body is null or empty, skip
|
||||||
|
// this
|
||||||
|
// 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)) {
|
||||||
|
logPrintStream.println("Error: " + response.getBody());
|
||||||
|
hasErrors = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
processOutputFiles(operation, file.getFilename(), response, newOutputFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasInputFileType) {
|
||||||
|
logPrintStream.println(
|
||||||
|
"No files with extension "
|
||||||
|
+ inputFileExtension
|
||||||
|
+ " found for operation "
|
||||||
|
+ operation);
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFiles = newOutputFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Filter and collect all files that match the inputFileExtension
|
||||||
|
List<Resource> matchingFiles =
|
||||||
|
outputFiles.stream()
|
||||||
|
.filter(
|
||||||
|
file ->
|
||||||
|
file.getFilename()
|
||||||
|
.endsWith(finalInputFileExtension))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// Check if there are matching files
|
||||||
|
if (!matchingFiles.isEmpty()) {
|
||||||
|
// Create a new MultiValueMap for the request body
|
||||||
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
|
||||||
|
// Add all matching files to the body
|
||||||
|
for (Resource file : matchingFiles) {
|
||||||
|
body.add("fileInput", file);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Entry<String, Object> entry : parameters.entrySet()) {
|
||||||
|
body.add(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
||||||
|
|
||||||
|
// Handle the response
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
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
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
String apiKey = getApiKeyForUser();
|
||||||
|
headers.add("X-API-Key", apiKey);
|
||||||
|
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||||
|
|
||||||
|
// Create HttpEntity with the body and headers
|
||||||
|
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
|
||||||
|
|
||||||
|
// Make the request to the REST endpoint
|
||||||
|
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
|
||||||
|
if (isZip(response.getBody())) {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newOutputFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Resource> generateInputFiles(File[] files) throws Exception {
|
||||||
|
if (files == null || files.length == 0) {
|
||||||
|
logger.info("No files");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Resource> outputFiles = new ArrayList<>();
|
||||||
|
|
||||||
|
for (File file : files) {
|
||||||
|
Path path = Paths.get(file.getAbsolutePath());
|
||||||
|
logger.info("Reading file: " + path); // debug statement
|
||||||
|
|
||||||
|
if (Files.exists(path)) {
|
||||||
|
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 {
|
||||||
|
if (files == null || files.length == 0) {
|
||||||
|
logger.info("No files");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Resource> outputFiles = new ArrayList<>();
|
||||||
|
|
||||||
|
for (MultipartFile file : files) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.pipeline;
|
||||||
|
|
||||||
|
public interface UserServiceInterface {
|
||||||
|
String getApiKeyForUser(String username);
|
||||||
|
}
|
||||||
@@ -53,6 +53,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 +62,228 @@ 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")
|
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign")
|
||||||
@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")
|
@Operation(
|
||||||
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) throws Exception {
|
summary = "Sign PDF with a Digital Certificate",
|
||||||
MultipartFile pdf = request.getFileInput();
|
description =
|
||||||
String certType = request.getCertType();
|
"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")
|
||||||
MultipartFile privateKeyFile = request.getPrivateKeyFile();
|
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request)
|
||||||
MultipartFile certFile = request.getCertFile();
|
throws Exception {
|
||||||
MultipartFile p12File = request.getP12File();
|
MultipartFile pdf = request.getFileInput();
|
||||||
String password = request.getPassword();
|
String certType = request.getCertType();
|
||||||
Boolean showSignature = request.isShowSignature();
|
MultipartFile privateKeyFile = request.getPrivateKeyFile();
|
||||||
String reason = request.getReason();
|
MultipartFile certFile = request.getCertFile();
|
||||||
String location = request.getLocation();
|
MultipartFile p12File = request.getP12File();
|
||||||
String name = request.getName();
|
String password = request.getPassword();
|
||||||
Integer pageNumber = request.getPageNumber();
|
Boolean showSignature = request.isShowSignature();
|
||||||
|
String reason = request.getReason();
|
||||||
|
String location = request.getLocation();
|
||||||
|
String name = request.getName();
|
||||||
|
Integer pageNumber = request.getPageNumber();
|
||||||
|
|
||||||
PrivateKey privateKey = null;
|
PrivateKey privateKey = null;
|
||||||
X509Certificate cert = null;
|
X509Certificate cert = null;
|
||||||
|
|
||||||
if (certType != null) {
|
if (certType != null) {
|
||||||
logger.info("Cert type provided: {}", certType);
|
logger.info("Cert type provided: {}", certType);
|
||||||
switch (certType) {
|
switch (certType) {
|
||||||
case "PKCS12":
|
case "PKCS12":
|
||||||
if (p12File != null) {
|
if (p12File != null) {
|
||||||
KeyStore ks = KeyStore.getInstance("PKCS12");
|
KeyStore ks = KeyStore.getInstance("PKCS12");
|
||||||
ks.load(new ByteArrayInputStream(p12File.getBytes()), password.toCharArray());
|
ks.load(
|
||||||
String alias = ks.aliases().nextElement();
|
new ByteArrayInputStream(p12File.getBytes()),
|
||||||
if (!ks.isKeyEntry(alias)) {
|
password.toCharArray());
|
||||||
throw new IllegalArgumentException("The provided PKCS12 file does not contain a private key.");
|
String alias = ks.aliases().nextElement();
|
||||||
}
|
if (!ks.isKeyEntry(alias)) {
|
||||||
privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray());
|
throw new IllegalArgumentException(
|
||||||
cert = (X509Certificate) ks.getCertificate(alias);
|
"The provided PKCS12 file does not contain a private key.");
|
||||||
}
|
}
|
||||||
break;
|
privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray());
|
||||||
case "PEM":
|
cert = (X509Certificate) ks.getCertificate(alias);
|
||||||
if (privateKeyFile != null && certFile != null) {
|
}
|
||||||
// Load private key
|
break;
|
||||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME);
|
case "PEM":
|
||||||
if (isPEM(privateKeyFile.getBytes())) {
|
if (privateKeyFile != null && certFile != null) {
|
||||||
privateKey = keyFactory
|
// Load private key
|
||||||
.generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes())));
|
KeyFactory keyFactory =
|
||||||
} else {
|
KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME);
|
||||||
privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes()));
|
if (isPEM(privateKeyFile.getBytes())) {
|
||||||
}
|
privateKey =
|
||||||
|
keyFactory.generatePrivate(
|
||||||
|
new PKCS8EncodedKeySpec(
|
||||||
|
parsePEM(privateKeyFile.getBytes())));
|
||||||
|
} else {
|
||||||
|
privateKey =
|
||||||
|
keyFactory.generatePrivate(
|
||||||
|
new PKCS8EncodedKeySpec(privateKeyFile.getBytes()));
|
||||||
|
}
|
||||||
|
|
||||||
// Load certificate
|
// Load certificate
|
||||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509",
|
CertificateFactory certFactory =
|
||||||
BouncyCastleProvider.PROVIDER_NAME);
|
CertificateFactory.getInstance(
|
||||||
if (isPEM(certFile.getBytes())) {
|
"X.509", BouncyCastleProvider.PROVIDER_NAME);
|
||||||
cert = (X509Certificate) certFactory
|
if (isPEM(certFile.getBytes())) {
|
||||||
.generateCertificate(new ByteArrayInputStream(parsePEM(certFile.getBytes())));
|
cert =
|
||||||
} else {
|
(X509Certificate)
|
||||||
cert = (X509Certificate) certFactory
|
certFactory.generateCertificate(
|
||||||
.generateCertificate(new ByteArrayInputStream(certFile.getBytes()));
|
new ByteArrayInputStream(
|
||||||
}
|
parsePEM(certFile.getBytes())));
|
||||||
}
|
} else {
|
||||||
break;
|
cert =
|
||||||
}
|
(X509Certificate)
|
||||||
}
|
certFactory.generateCertificate(
|
||||||
PDSignature signature = new PDSignature();
|
new ByteArrayInputStream(certFile.getBytes()));
|
||||||
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); // default filter
|
}
|
||||||
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_SHA1);
|
}
|
||||||
signature.setName(name);
|
break;
|
||||||
signature.setLocation(location);
|
}
|
||||||
signature.setReason(reason);
|
}
|
||||||
signature.setSignDate(Calendar.getInstance());
|
PDSignature signature = new PDSignature();
|
||||||
|
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); // default filter
|
||||||
// Load the PDF
|
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_SHA1);
|
||||||
try (PDDocument document = PDDocument.load(pdf.getBytes())) {
|
signature.setName(name);
|
||||||
logger.info("Successfully loaded the provided PDF");
|
signature.setLocation(location);
|
||||||
SignatureOptions signatureOptions = new SignatureOptions();
|
signature.setReason(reason);
|
||||||
|
signature.setSignDate(Calendar.getInstance());
|
||||||
|
|
||||||
// If you want to show the signature
|
// Load the PDF
|
||||||
|
try (PDDocument document = PDDocument.load(pdf.getBytes())) {
|
||||||
|
logger.info("Successfully loaded the provided PDF");
|
||||||
|
SignatureOptions signatureOptions = new SignatureOptions();
|
||||||
|
|
||||||
// ATTEMPT 2
|
// If you want to show the signature
|
||||||
if (showSignature != null && showSignature) {
|
|
||||||
PDPage page = document.getPage(pageNumber - 1);
|
|
||||||
|
|
||||||
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
|
// ATTEMPT 2
|
||||||
if (acroForm == null) {
|
if (showSignature != null && showSignature) {
|
||||||
acroForm = new PDAcroForm(document);
|
PDPage page = document.getPage(pageNumber - 1);
|
||||||
document.getDocumentCatalog().setAcroForm(acroForm);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new signature field and widget
|
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
|
||||||
|
if (acroForm == null) {
|
||||||
|
acroForm = new PDAcroForm(document);
|
||||||
|
document.getDocumentCatalog().setAcroForm(acroForm);
|
||||||
|
}
|
||||||
|
|
||||||
PDSignatureField signatureField = new PDSignatureField(acroForm);
|
// Create a new signature field and widget
|
||||||
PDAnnotationWidget widget = signatureField.getWidgets().get(0);
|
|
||||||
PDRectangle rect = new PDRectangle(100, 100, 200, 50); // Define the rectangle size here
|
|
||||||
widget.setRectangle(rect);
|
|
||||||
page.getAnnotations().add(widget);
|
|
||||||
|
|
||||||
// Set the appearance for the signature field
|
PDSignatureField signatureField = new PDSignatureField(acroForm);
|
||||||
PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary();
|
PDAnnotationWidget widget = signatureField.getWidgets().get(0);
|
||||||
PDAppearanceStream appearanceStream = new PDAppearanceStream(document);
|
PDRectangle rect =
|
||||||
appearanceStream.setResources(new PDResources());
|
new PDRectangle(100, 100, 200, 50); // Define the rectangle size here
|
||||||
appearanceStream.setBBox(rect);
|
widget.setRectangle(rect);
|
||||||
appearanceDict.setNormalAppearance(appearanceStream);
|
page.getAnnotations().add(widget);
|
||||||
widget.setAppearance(appearanceDict);
|
|
||||||
|
|
||||||
try (PDPageContentStream contentStream = new PDPageContentStream(document, appearanceStream)) {
|
// Set the appearance for the signature field
|
||||||
contentStream.beginText();
|
PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary();
|
||||||
contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12);
|
PDAppearanceStream appearanceStream = new PDAppearanceStream(document);
|
||||||
contentStream.newLineAtOffset(110, 130);
|
appearanceStream.setResources(new PDResources());
|
||||||
contentStream.showText("Digitally signed by: " + (name != null ? name : "Unknown"));
|
appearanceStream.setBBox(rect);
|
||||||
contentStream.newLineAtOffset(0, -15);
|
appearanceDict.setNormalAppearance(appearanceStream);
|
||||||
contentStream.showText("Date: " + new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z").format(new Date()));
|
widget.setAppearance(appearanceDict);
|
||||||
contentStream.newLineAtOffset(0, -15);
|
|
||||||
if (reason != null && !reason.isEmpty()) {
|
|
||||||
contentStream.showText("Reason: " + reason);
|
|
||||||
contentStream.newLineAtOffset(0, -15);
|
|
||||||
}
|
|
||||||
if (location != null && !location.isEmpty()) {
|
|
||||||
contentStream.showText("Location: " + location);
|
|
||||||
contentStream.newLineAtOffset(0, -15);
|
|
||||||
}
|
|
||||||
contentStream.endText();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the widget annotation to the page
|
try (PDPageContentStream contentStream =
|
||||||
page.getAnnotations().add(widget);
|
new PDPageContentStream(document, appearanceStream)) {
|
||||||
|
contentStream.beginText();
|
||||||
|
contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12);
|
||||||
|
contentStream.newLineAtOffset(110, 130);
|
||||||
|
contentStream.showText(
|
||||||
|
"Digitally signed by: " + (name != null ? name : "Unknown"));
|
||||||
|
contentStream.newLineAtOffset(0, -15);
|
||||||
|
contentStream.showText(
|
||||||
|
"Date: "
|
||||||
|
+ new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z")
|
||||||
|
.format(new Date()));
|
||||||
|
contentStream.newLineAtOffset(0, -15);
|
||||||
|
if (reason != null && !reason.isEmpty()) {
|
||||||
|
contentStream.showText("Reason: " + reason);
|
||||||
|
contentStream.newLineAtOffset(0, -15);
|
||||||
|
}
|
||||||
|
if (location != null && !location.isEmpty()) {
|
||||||
|
contentStream.showText("Location: " + location);
|
||||||
|
contentStream.newLineAtOffset(0, -15);
|
||||||
|
}
|
||||||
|
contentStream.endText();
|
||||||
|
}
|
||||||
|
|
||||||
// Add the signature field to the acroform
|
// Add the widget annotation to the page
|
||||||
acroForm.getFields().add(signatureField);
|
page.getAnnotations().add(widget);
|
||||||
|
|
||||||
// Handle multiple signatures by ensuring a unique field name
|
// Add the signature field to the acroform
|
||||||
String baseFieldName = "Signature";
|
acroForm.getFields().add(signatureField);
|
||||||
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());
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
// Using BouncyCastle to sign
|
document.addSignature(signature, signatureOptions);
|
||||||
CMSTypedData cmsData = new CMSProcessableByteArray(content);
|
logger.info("Signature added to the PDF document");
|
||||||
|
// External signing
|
||||||
|
ExternalSigningSupport externalSigning =
|
||||||
|
document.saveIncrementalForExternalSigning(new ByteArrayOutputStream());
|
||||||
|
|
||||||
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
|
byte[] content = IOUtils.toByteArray(externalSigning.getContent());
|
||||||
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
|
|
||||||
.setProvider(BouncyCastleProvider.PROVIDER_NAME).build(privateKey);
|
|
||||||
|
|
||||||
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(
|
// Using BouncyCastle to sign
|
||||||
new JcaDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build())
|
CMSTypedData cmsData = new CMSProcessableByteArray(content);
|
||||||
.build(signer, cert));
|
|
||||||
|
|
||||||
gen.addCertificates(new JcaCertStore(Collections.singletonList(cert)));
|
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
|
||||||
CMSSignedData signedData = gen.generate(cmsData, false);
|
ContentSigner signer =
|
||||||
|
new JcaContentSignerBuilder("SHA256withRSA")
|
||||||
|
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
|
.build(privateKey);
|
||||||
|
|
||||||
byte[] cmsSignature = signedData.getEncoded();
|
gen.addSignerInfoGenerator(
|
||||||
logger.info("About to sign content using BouncyCastle");
|
new JcaSignerInfoGeneratorBuilder(
|
||||||
externalSigning.setSignature(cmsSignature);
|
new JcaDigestCalculatorProviderBuilder()
|
||||||
logger.info("Signature set successfully");
|
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
|
.build())
|
||||||
|
.build(signer, cert));
|
||||||
|
|
||||||
// After setting the signature, return the resultant PDF
|
gen.addCertificates(new JcaCertStore(Collections.singletonList(cert)));
|
||||||
try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) {
|
CMSSignedData signedData = gen.generate(cmsData, false);
|
||||||
document.save(signedPdfOutput);
|
|
||||||
return WebResponseUtils.boasToWebResponse(signedPdfOutput,
|
|
||||||
pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_signed.pdf");
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
byte[] cmsSignature = signedData.getEncoded();
|
||||||
e.printStackTrace();
|
logger.info("About to sign content using BouncyCastle");
|
||||||
}
|
externalSigning.setSignature(cmsSignature);
|
||||||
} catch (Exception e) {
|
logger.info("Signature set successfully");
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
// 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");
|
||||||
|
|
||||||
private byte[] parsePEM(byte[] content) throws IOException {
|
} catch (Exception e) {
|
||||||
PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(content)));
|
e.printStackTrace();
|
||||||
return pemReader.readPemObject().getContent();
|
}
|
||||||
}
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isPEM(byte[] content) {
|
return null;
|
||||||
String contentStr = new String(content);
|
}
|
||||||
return contentStr.contains("-----BEGIN") && contentStr.contains("-----END");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,23 +72,22 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
|||||||
|
|
||||||
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/security")
|
@RequestMapping("/api/v1/security")
|
||||||
@Tag(name = "Security", description = "Security APIs")
|
@Tag(name = "Security", description = "Security APIs")
|
||||||
public class GetInfoOnPDF {
|
public class GetInfoOnPDF {
|
||||||
|
|
||||||
static ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf")
|
static ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf")
|
||||||
@Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO")
|
@Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO")
|
||||||
public ResponseEntity<byte[]> getPdfInfo(@ModelAttribute PDFFile request)
|
public ResponseEntity<byte[]> getPdfInfo(@ModelAttribute PDFFile request) throws IOException {
|
||||||
throws IOException {
|
MultipartFile inputFile = request.getFileInput();
|
||||||
MultipartFile inputFile = request.getFileInput();
|
try (PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream()); ) {
|
||||||
try (
|
|
||||||
PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream());
|
|
||||||
) {
|
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
ObjectNode jsonOutput = objectMapper.createObjectNode();
|
ObjectNode jsonOutput = objectMapper.createObjectNode();
|
||||||
|
|
||||||
@@ -100,8 +99,7 @@ public class GetInfoOnPDF {
|
|||||||
ObjectNode compliancy = objectMapper.createObjectNode();
|
ObjectNode compliancy = objectMapper.createObjectNode();
|
||||||
ObjectNode encryption = objectMapper.createObjectNode();
|
ObjectNode encryption = objectMapper.createObjectNode();
|
||||||
ObjectNode other = objectMapper.createObjectNode();
|
ObjectNode other = objectMapper.createObjectNode();
|
||||||
|
|
||||||
|
|
||||||
metadata.put("Title", info.getTitle());
|
metadata.put("Title", info.getTitle());
|
||||||
metadata.put("Author", info.getAuthor());
|
metadata.put("Author", info.getAuthor());
|
||||||
metadata.put("Subject", info.getSubject());
|
metadata.put("Subject", info.getSubject());
|
||||||
@@ -111,14 +109,11 @@ public class GetInfoOnPDF {
|
|||||||
metadata.put("CreationDate", formatDate(info.getCreationDate()));
|
metadata.put("CreationDate", formatDate(info.getCreationDate()));
|
||||||
metadata.put("ModificationDate", formatDate(info.getModificationDate()));
|
metadata.put("ModificationDate", formatDate(info.getModificationDate()));
|
||||||
jsonOutput.set("Metadata", metadata);
|
jsonOutput.set("Metadata", metadata);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Total file size of the PDF
|
// Total file size of the PDF
|
||||||
long fileSizeInBytes = inputFile.getSize();
|
long fileSizeInBytes = inputFile.getSize();
|
||||||
basicInfo.put("FileSizeInBytes", fileSizeInBytes);
|
basicInfo.put("FileSizeInBytes", fileSizeInBytes);
|
||||||
|
|
||||||
// Number of words, paragraphs, and images in the entire document
|
// Number of words, paragraphs, and images in the entire document
|
||||||
String fullText = new PDFTextStripper().getText(pdfBoxDoc);
|
String fullText = new PDFTextStripper().getText(pdfBoxDoc);
|
||||||
String[] words = fullText.split("\\s+");
|
String[] words = fullText.split("\\s+");
|
||||||
@@ -129,8 +124,7 @@ public class GetInfoOnPDF {
|
|||||||
// Number of characters in the entire document (including spaces and special characters)
|
// Number of characters in the entire document (including spaces and special characters)
|
||||||
int charCount = fullText.length();
|
int charCount = fullText.length();
|
||||||
basicInfo.put("CharacterCount", charCount);
|
basicInfo.put("CharacterCount", charCount);
|
||||||
|
|
||||||
|
|
||||||
// Initialize the flags and types
|
// Initialize the flags and types
|
||||||
boolean hasCompression = false;
|
boolean hasCompression = false;
|
||||||
String compressionType = "None";
|
String compressionType = "None";
|
||||||
@@ -147,26 +141,21 @@ public class GetInfoOnPDF {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
basicInfo.put("Compression", hasCompression);
|
basicInfo.put("Compression", hasCompression);
|
||||||
if(hasCompression)
|
if (hasCompression) basicInfo.put("CompressionType", compressionType);
|
||||||
basicInfo.put("CompressionType", compressionType);
|
|
||||||
|
|
||||||
String language = pdfBoxDoc.getDocumentCatalog().getLanguage();
|
String language = pdfBoxDoc.getDocumentCatalog().getLanguage();
|
||||||
basicInfo.put("Language", language);
|
basicInfo.put("Language", language);
|
||||||
basicInfo.put("Number of pages", pdfBoxDoc.getNumberOfPages());
|
basicInfo.put("Number of pages", pdfBoxDoc.getNumberOfPages());
|
||||||
|
|
||||||
|
|
||||||
PDDocumentCatalog catalog = pdfBoxDoc.getDocumentCatalog();
|
PDDocumentCatalog catalog = pdfBoxDoc.getDocumentCatalog();
|
||||||
String pageMode = catalog.getPageMode().name();
|
String pageMode = catalog.getPageMode().name();
|
||||||
|
|
||||||
// Document Information using PDFBox
|
// Document Information using PDFBox
|
||||||
docInfoNode.put("PDF version", pdfBoxDoc.getVersion());
|
docInfoNode.put("PDF version", pdfBoxDoc.getVersion());
|
||||||
docInfoNode.put("Trapped", info.getTrapped());
|
docInfoNode.put("Trapped", info.getTrapped());
|
||||||
docInfoNode.put("Page Mode", getPageModeDescription(pageMode));;
|
docInfoNode.put("Page Mode", getPageModeDescription(pageMode));
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
PDAcroForm acroForm = pdfBoxDoc.getDocumentCatalog().getAcroForm();
|
PDAcroForm acroForm = pdfBoxDoc.getDocumentCatalog().getAcroForm();
|
||||||
|
|
||||||
ObjectNode formFieldsNode = objectMapper.createObjectNode();
|
ObjectNode formFieldsNode = objectMapper.createObjectNode();
|
||||||
@@ -177,41 +166,37 @@ public class GetInfoOnPDF {
|
|||||||
}
|
}
|
||||||
jsonOutput.set("FormFields", formFieldsNode);
|
jsonOutput.set("FormFields", formFieldsNode);
|
||||||
|
|
||||||
|
// embeed files TODO size
|
||||||
|
if (catalog.getNames() != null) {
|
||||||
|
PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles();
|
||||||
|
|
||||||
|
ArrayNode embeddedFilesArray = objectMapper.createArrayNode();
|
||||||
//embeed files TODO size
|
if (efTree != null) {
|
||||||
if(catalog.getNames() != null) {
|
Map<String, PDComplexFileSpecification> efMap = efTree.getNames();
|
||||||
PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles();
|
if (efMap != null) {
|
||||||
|
for (Map.Entry<String, PDComplexFileSpecification> entry :
|
||||||
ArrayNode embeddedFilesArray = objectMapper.createArrayNode();
|
efMap.entrySet()) {
|
||||||
if (efTree != null) {
|
ObjectNode embeddedFileNode = objectMapper.createObjectNode();
|
||||||
Map<String, PDComplexFileSpecification> efMap = efTree.getNames();
|
embeddedFileNode.put("Name", entry.getKey());
|
||||||
if (efMap != null) {
|
PDEmbeddedFile embeddedFile = entry.getValue().getEmbeddedFile();
|
||||||
for (Map.Entry<String, PDComplexFileSpecification> entry : efMap.entrySet()) {
|
if (embeddedFile != null) {
|
||||||
ObjectNode embeddedFileNode = objectMapper.createObjectNode();
|
embeddedFileNode.put(
|
||||||
embeddedFileNode.put("Name", entry.getKey());
|
"FileSize", embeddedFile.getLength()); // size in bytes
|
||||||
PDEmbeddedFile embeddedFile = entry.getValue().getEmbeddedFile();
|
}
|
||||||
if (embeddedFile != null) {
|
embeddedFilesArray.add(embeddedFileNode);
|
||||||
embeddedFileNode.put("FileSize", embeddedFile.getLength()); // size in bytes
|
}
|
||||||
}
|
}
|
||||||
embeddedFilesArray.add(embeddedFileNode);
|
}
|
||||||
}
|
other.set("EmbeddedFiles", embeddedFilesArray);
|
||||||
}
|
|
||||||
}
|
|
||||||
other.set("EmbeddedFiles", embeddedFilesArray);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// attachments TODO size
|
||||||
|
|
||||||
//attachments TODO size
|
|
||||||
ArrayNode attachmentsArray = objectMapper.createArrayNode();
|
ArrayNode attachmentsArray = objectMapper.createArrayNode();
|
||||||
for (PDPage page : pdfBoxDoc.getPages()) {
|
for (PDPage page : pdfBoxDoc.getPages()) {
|
||||||
for (PDAnnotation annotation : page.getAnnotations()) {
|
for (PDAnnotation annotation : page.getAnnotations()) {
|
||||||
if (annotation instanceof PDAnnotationFileAttachment) {
|
if (annotation instanceof PDAnnotationFileAttachment) {
|
||||||
PDAnnotationFileAttachment fileAttachmentAnnotation = (PDAnnotationFileAttachment) annotation;
|
PDAnnotationFileAttachment fileAttachmentAnnotation =
|
||||||
|
(PDAnnotationFileAttachment) annotation;
|
||||||
|
|
||||||
ObjectNode attachmentNode = objectMapper.createObjectNode();
|
ObjectNode attachmentNode = objectMapper.createObjectNode();
|
||||||
attachmentNode.put("Name", fileAttachmentAnnotation.getAttachmentName());
|
attachmentNode.put("Name", fileAttachmentAnnotation.getAttachmentName());
|
||||||
@@ -223,7 +208,7 @@ public class GetInfoOnPDF {
|
|||||||
}
|
}
|
||||||
other.set("Attachments", attachmentsArray);
|
other.set("Attachments", attachmentsArray);
|
||||||
|
|
||||||
//Javascript
|
// Javascript
|
||||||
PDDocumentNameDictionary namesDict = catalog.getNames();
|
PDDocumentNameDictionary namesDict = catalog.getNames();
|
||||||
ArrayNode javascriptArray = objectMapper.createArrayNode();
|
ArrayNode javascriptArray = objectMapper.createArrayNode();
|
||||||
|
|
||||||
@@ -254,9 +239,9 @@ public class GetInfoOnPDF {
|
|||||||
}
|
}
|
||||||
other.set("JavaScript", javascriptArray);
|
other.set("JavaScript", javascriptArray);
|
||||||
|
|
||||||
|
// TODO size
|
||||||
//TODO size
|
PDOptionalContentProperties ocProperties =
|
||||||
PDOptionalContentProperties ocProperties = pdfBoxDoc.getDocumentCatalog().getOCProperties();
|
pdfBoxDoc.getDocumentCatalog().getOCProperties();
|
||||||
ArrayNode layersArray = objectMapper.createArrayNode();
|
ArrayNode layersArray = objectMapper.createArrayNode();
|
||||||
|
|
||||||
if (ocProperties != null) {
|
if (ocProperties != null) {
|
||||||
@@ -268,34 +253,38 @@ public class GetInfoOnPDF {
|
|||||||
}
|
}
|
||||||
|
|
||||||
other.set("Layers", layersArray);
|
other.set("Layers", layersArray);
|
||||||
|
|
||||||
//TODO Security
|
|
||||||
|
|
||||||
|
|
||||||
|
// TODO Security
|
||||||
|
|
||||||
|
PDStructureTreeRoot structureTreeRoot =
|
||||||
|
pdfBoxDoc.getDocumentCatalog().getStructureTreeRoot();
|
||||||
PDStructureTreeRoot structureTreeRoot = pdfBoxDoc.getDocumentCatalog().getStructureTreeRoot();
|
|
||||||
ArrayNode structureTreeArray;
|
ArrayNode structureTreeArray;
|
||||||
try {
|
try {
|
||||||
if(structureTreeRoot != null) {
|
if (structureTreeRoot != null) {
|
||||||
structureTreeArray = exploreStructureTree(structureTreeRoot.getKids());
|
structureTreeArray = exploreStructureTree(structureTreeRoot.getKids());
|
||||||
other.set("StructureTree", structureTreeArray);
|
other.set("StructureTree", structureTreeArray);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// TODO Auto-generated catch block
|
// TODO Auto-generated catch block
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A");
|
boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A");
|
||||||
boolean isPdfXCompliant = checkForStandard(pdfBoxDoc, "PDF/X");
|
boolean isPdfXCompliant = checkForStandard(pdfBoxDoc, "PDF/X");
|
||||||
boolean isPdfECompliant = checkForStandard(pdfBoxDoc, "PDF/E");
|
boolean isPdfECompliant = checkForStandard(pdfBoxDoc, "PDF/E");
|
||||||
boolean isPdfVTCompliant = checkForStandard(pdfBoxDoc, "PDF/VT");
|
boolean isPdfVTCompliant = checkForStandard(pdfBoxDoc, "PDF/VT");
|
||||||
boolean isPdfUACompliant = checkForStandard(pdfBoxDoc, "PDF/UA");
|
boolean isPdfUACompliant = checkForStandard(pdfBoxDoc, "PDF/UA");
|
||||||
boolean isPdfBCompliant = checkForStandard(pdfBoxDoc, "PDF/B"); // If you want to check for PDF/Broadcast, though this isn't an official ISO standard.
|
boolean isPdfBCompliant =
|
||||||
boolean isPdfSECCompliant = checkForStandard(pdfBoxDoc, "PDF/SEC"); // This might not be effective since PDF/SEC was under development in 2021.
|
checkForStandard(
|
||||||
|
pdfBoxDoc,
|
||||||
|
"PDF/B"); // If you want to check for PDF/Broadcast, though this isn't
|
||||||
|
// an official ISO standard.
|
||||||
|
boolean isPdfSECCompliant =
|
||||||
|
checkForStandard(
|
||||||
|
pdfBoxDoc,
|
||||||
|
"PDF/SEC"); // This might not be effective since PDF/SEC was under
|
||||||
|
// development in 2021.
|
||||||
|
|
||||||
compliancy.put("IsPDF/ACompliant", isPdfACompliant);
|
compliancy.put("IsPDF/ACompliant", isPdfACompliant);
|
||||||
compliancy.put("IsPDF/XCompliant", isPdfXCompliant);
|
compliancy.put("IsPDF/XCompliant", isPdfXCompliant);
|
||||||
compliancy.put("IsPDF/ECompliant", isPdfECompliant);
|
compliancy.put("IsPDF/ECompliant", isPdfECompliant);
|
||||||
@@ -304,10 +293,6 @@ public class GetInfoOnPDF {
|
|||||||
compliancy.put("IsPDF/BCompliant", isPdfBCompliant);
|
compliancy.put("IsPDF/BCompliant", isPdfBCompliant);
|
||||||
compliancy.put("IsPDF/SECCompliant", isPdfSECCompliant);
|
compliancy.put("IsPDF/SECCompliant", isPdfSECCompliant);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
PDOutlineNode root = pdfBoxDoc.getDocumentCatalog().getDocumentOutline();
|
PDOutlineNode root = pdfBoxDoc.getDocumentCatalog().getDocumentOutline();
|
||||||
ArrayNode bookmarksArray = objectMapper.createArrayNode();
|
ArrayNode bookmarksArray = objectMapper.createArrayNode();
|
||||||
|
|
||||||
@@ -318,33 +303,29 @@ public class GetInfoOnPDF {
|
|||||||
}
|
}
|
||||||
|
|
||||||
other.set("Bookmarks/Outline/TOC", bookmarksArray);
|
other.set("Bookmarks/Outline/TOC", bookmarksArray);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
PDMetadata pdMetadata = pdfBoxDoc.getDocumentCatalog().getMetadata();
|
PDMetadata pdMetadata = pdfBoxDoc.getDocumentCatalog().getMetadata();
|
||||||
|
|
||||||
String xmpString = null;
|
String xmpString = null;
|
||||||
|
|
||||||
if (pdMetadata != null) {
|
if (pdMetadata != null) {
|
||||||
try {
|
try {
|
||||||
COSInputStream is = pdMetadata.createInputStream();
|
COSInputStream is = pdMetadata.createInputStream();
|
||||||
DomXmpParser domXmpParser = new DomXmpParser();
|
DomXmpParser domXmpParser = new DomXmpParser();
|
||||||
XMPMetadata xmpMeta = domXmpParser.parse(is);
|
XMPMetadata xmpMeta = domXmpParser.parse(is);
|
||||||
|
|
||||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
new XmpSerializer().serialize(xmpMeta, os, true);
|
new XmpSerializer().serialize(xmpMeta, os, true);
|
||||||
xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8);
|
xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8);
|
||||||
} catch (XmpParsingException | IOException e) {
|
} catch (XmpParsingException | IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
other.put("XMPMetadata", xmpString);
|
other.put("XMPMetadata", xmpString);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (pdfBoxDoc.isEncrypted()) {
|
if (pdfBoxDoc.isEncrypted()) {
|
||||||
encryption.put("IsEncrypted", true);
|
encryption.put("IsEncrypted", true);
|
||||||
|
|
||||||
// Retrieve encryption details using getEncryption()
|
// Retrieve encryption details using getEncryption()
|
||||||
PDEncryption pdfEncryption = pdfBoxDoc.getEncryption();
|
PDEncryption pdfEncryption = pdfBoxDoc.getEncryption();
|
||||||
@@ -353,31 +334,30 @@ public class GetInfoOnPDF {
|
|||||||
AccessPermission ap = pdfBoxDoc.getCurrentAccessPermission();
|
AccessPermission ap = pdfBoxDoc.getCurrentAccessPermission();
|
||||||
if (ap != null) {
|
if (ap != null) {
|
||||||
ObjectNode permissionsNode = objectMapper.createObjectNode();
|
ObjectNode permissionsNode = objectMapper.createObjectNode();
|
||||||
|
|
||||||
permissionsNode.put("CanAssembleDocument", ap.canAssembleDocument());
|
permissionsNode.put("CanAssembleDocument", ap.canAssembleDocument());
|
||||||
permissionsNode.put("CanExtractContent", ap.canExtractContent());
|
permissionsNode.put("CanExtractContent", ap.canExtractContent());
|
||||||
permissionsNode.put("CanExtractForAccessibility", ap.canExtractForAccessibility());
|
permissionsNode.put(
|
||||||
|
"CanExtractForAccessibility", ap.canExtractForAccessibility());
|
||||||
permissionsNode.put("CanFillInForm", ap.canFillInForm());
|
permissionsNode.put("CanFillInForm", ap.canFillInForm());
|
||||||
permissionsNode.put("CanModify", ap.canModify());
|
permissionsNode.put("CanModify", ap.canModify());
|
||||||
permissionsNode.put("CanModifyAnnotations", ap.canModifyAnnotations());
|
permissionsNode.put("CanModifyAnnotations", ap.canModifyAnnotations());
|
||||||
permissionsNode.put("CanPrint", ap.canPrint());
|
permissionsNode.put("CanPrint", ap.canPrint());
|
||||||
permissionsNode.put("CanPrintDegraded", ap.canPrintDegraded());
|
permissionsNode.put("CanPrintDegraded", ap.canPrintDegraded());
|
||||||
|
|
||||||
encryption.set("Permissions", permissionsNode); // set the node under "Permissions"
|
encryption.set(
|
||||||
}
|
"Permissions", permissionsNode); // set the node under "Permissions"
|
||||||
|
}
|
||||||
// Add other encryption-related properties as needed
|
// Add other encryption-related properties as needed
|
||||||
} else {
|
} else {
|
||||||
encryption.put("IsEncrypted", false);
|
encryption.put("IsEncrypted", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ObjectNode pageInfoParent = objectMapper.createObjectNode();
|
ObjectNode pageInfoParent = objectMapper.createObjectNode();
|
||||||
for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) {
|
for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) {
|
||||||
ObjectNode pageInfo = objectMapper.createObjectNode();
|
ObjectNode pageInfo = objectMapper.createObjectNode();
|
||||||
|
|
||||||
// Retrieve the page
|
// Retrieve the page
|
||||||
PDPage page = pdfBoxDoc.getPage(pageNum);
|
PDPage page = pdfBoxDoc.getPage(pageNum);
|
||||||
|
|
||||||
// Page-level Information
|
// Page-level Information
|
||||||
@@ -387,20 +367,20 @@ public class GetInfoOnPDF {
|
|||||||
float height = mediaBox.getHeight();
|
float height = mediaBox.getHeight();
|
||||||
|
|
||||||
ObjectNode sizeInfo = objectMapper.createObjectNode();
|
ObjectNode sizeInfo = objectMapper.createObjectNode();
|
||||||
|
|
||||||
getDimensionInfo(sizeInfo, width, height);
|
getDimensionInfo(sizeInfo, width, height);
|
||||||
|
|
||||||
sizeInfo.put("Standard Page", getPageSize(width, height));
|
sizeInfo.put("Standard Page", getPageSize(width, height));
|
||||||
pageInfo.set("Size", sizeInfo);
|
pageInfo.set("Size", sizeInfo);
|
||||||
|
|
||||||
pageInfo.put("Rotation", page.getRotation());
|
pageInfo.put("Rotation", page.getRotation());
|
||||||
pageInfo.put("Page Orientation", getPageOrientation(width, height));
|
pageInfo.put("Page Orientation", getPageOrientation(width, height));
|
||||||
|
|
||||||
|
|
||||||
// Boxes
|
// Boxes
|
||||||
pageInfo.put("MediaBox", mediaBox.toString());
|
pageInfo.put("MediaBox", mediaBox.toString());
|
||||||
|
|
||||||
// Assuming the following boxes are defined for your document; if not, you may get null values.
|
// Assuming the following boxes are defined for your document; if not, you may get
|
||||||
|
// null values.
|
||||||
PDRectangle cropBox = page.getCropBox();
|
PDRectangle cropBox = page.getCropBox();
|
||||||
pageInfo.put("CropBox", cropBox == null ? "Undefined" : cropBox.toString());
|
pageInfo.put("CropBox", cropBox == null ? "Undefined" : cropBox.toString());
|
||||||
|
|
||||||
@@ -416,13 +396,13 @@ public class GetInfoOnPDF {
|
|||||||
// Content Extraction
|
// Content Extraction
|
||||||
PDFTextStripper textStripper = new PDFTextStripper();
|
PDFTextStripper textStripper = new PDFTextStripper();
|
||||||
textStripper.setStartPage(pageNum + 1);
|
textStripper.setStartPage(pageNum + 1);
|
||||||
textStripper.setEndPage(pageNum +1);
|
textStripper.setEndPage(pageNum + 1);
|
||||||
String pageText = textStripper.getText(pdfBoxDoc);
|
String pageText = textStripper.getText(pdfBoxDoc);
|
||||||
|
|
||||||
pageInfo.put("Text Characters Count", pageText.length()); //
|
pageInfo.put("Text Characters Count", pageText.length()); //
|
||||||
|
|
||||||
// Annotations
|
// Annotations
|
||||||
|
|
||||||
List<PDAnnotation> annotations = page.getAnnotations();
|
List<PDAnnotation> annotations = page.getAnnotations();
|
||||||
|
|
||||||
int subtypeCount = 0;
|
int subtypeCount = 0;
|
||||||
@@ -430,10 +410,10 @@ public class GetInfoOnPDF {
|
|||||||
|
|
||||||
for (PDAnnotation annotation : annotations) {
|
for (PDAnnotation annotation : annotations) {
|
||||||
if (annotation.getSubtype() != null) {
|
if (annotation.getSubtype() != null) {
|
||||||
subtypeCount++; // Increase subtype count
|
subtypeCount++; // Increase subtype count
|
||||||
}
|
}
|
||||||
if (annotation.getContents() != null) {
|
if (annotation.getContents() != null) {
|
||||||
contentsCount++; // Increase contents count
|
contentsCount++; // Increase contents count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,26 +422,25 @@ public class GetInfoOnPDF {
|
|||||||
annotationsObject.put("SubtypeCount", subtypeCount);
|
annotationsObject.put("SubtypeCount", subtypeCount);
|
||||||
annotationsObject.put("ContentsCount", contentsCount);
|
annotationsObject.put("ContentsCount", contentsCount);
|
||||||
pageInfo.set("Annotations", annotationsObject);
|
pageInfo.set("Annotations", annotationsObject);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Images (simplified)
|
// Images (simplified)
|
||||||
// This part is non-trivial as images can be embedded in multiple ways in a PDF.
|
// This part is non-trivial as images can be embedded in multiple ways in a PDF.
|
||||||
// Here is a basic structure to recognize image XObjects on a page.
|
// Here is a basic structure to recognize image XObjects on a page.
|
||||||
ArrayNode imagesArray = objectMapper.createArrayNode();
|
ArrayNode imagesArray = objectMapper.createArrayNode();
|
||||||
PDResources resources = page.getResources();
|
PDResources resources = page.getResources();
|
||||||
|
|
||||||
|
|
||||||
for (COSName name : resources.getXObjectNames()) {
|
for (COSName name : resources.getXObjectNames()) {
|
||||||
PDXObject xObject = resources.getXObject(name);
|
PDXObject xObject = resources.getXObject(name);
|
||||||
if (xObject instanceof PDImageXObject) {
|
if (xObject instanceof PDImageXObject) {
|
||||||
PDImageXObject image = (PDImageXObject) xObject;
|
PDImageXObject image = (PDImageXObject) xObject;
|
||||||
|
|
||||||
ObjectNode imageNode = objectMapper.createObjectNode();
|
ObjectNode imageNode = objectMapper.createObjectNode();
|
||||||
imageNode.put("Width", image.getWidth());
|
imageNode.put("Width", image.getWidth());
|
||||||
imageNode.put("Height", image.getHeight());
|
imageNode.put("Height", image.getHeight());
|
||||||
if(image.getMetadata() != null && image.getMetadata().getFile() != null && image.getMetadata().getFile().getFile() != null) {
|
if (image.getMetadata() != null
|
||||||
imageNode.put("Name", image.getMetadata().getFile().getFile());
|
&& image.getMetadata().getFile() != null
|
||||||
|
&& image.getMetadata().getFile().getFile() != null) {
|
||||||
|
imageNode.put("Name", image.getMetadata().getFile().getFile());
|
||||||
}
|
}
|
||||||
if (image.getColorSpace() != null) {
|
if (image.getColorSpace() != null) {
|
||||||
imageNode.put("ColorSpace", image.getColorSpace().getName());
|
imageNode.put("ColorSpace", image.getColorSpace().getName());
|
||||||
@@ -472,10 +451,9 @@ public class GetInfoOnPDF {
|
|||||||
}
|
}
|
||||||
pageInfo.set("Images", imagesArray);
|
pageInfo.set("Images", imagesArray);
|
||||||
|
|
||||||
|
|
||||||
// Links
|
// Links
|
||||||
ArrayNode linksArray = objectMapper.createArrayNode();
|
ArrayNode linksArray = objectMapper.createArrayNode();
|
||||||
Set<String> uniqueURIs = new HashSet<>(); // To store unique URIs
|
Set<String> uniqueURIs = new HashSet<>(); // To store unique URIs
|
||||||
|
|
||||||
for (PDAnnotation annotation : annotations) {
|
for (PDAnnotation annotation : annotations) {
|
||||||
if (annotation instanceof PDAnnotationLink) {
|
if (annotation instanceof PDAnnotationLink) {
|
||||||
@@ -483,7 +461,7 @@ public class GetInfoOnPDF {
|
|||||||
if (linkAnnotation.getAction() instanceof PDActionURI) {
|
if (linkAnnotation.getAction() instanceof PDActionURI) {
|
||||||
PDActionURI uriAction = (PDActionURI) linkAnnotation.getAction();
|
PDActionURI uriAction = (PDActionURI) linkAnnotation.getAction();
|
||||||
String uri = uriAction.getURI();
|
String uri = uriAction.getURI();
|
||||||
uniqueURIs.add(uri); // Add to set to ensure uniqueness
|
uniqueURIs.add(uri); // Add to set to ensure uniqueness
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,8 +473,7 @@ public class GetInfoOnPDF {
|
|||||||
linksArray.add(linkNode);
|
linksArray.add(linkNode);
|
||||||
}
|
}
|
||||||
pageInfo.set("Links", linksArray);
|
pageInfo.set("Links", linksArray);
|
||||||
|
|
||||||
|
|
||||||
// Fonts
|
// Fonts
|
||||||
ArrayNode fontsArray = objectMapper.createArrayNode();
|
ArrayNode fontsArray = objectMapper.createArrayNode();
|
||||||
Map<String, ObjectNode> uniqueFontsMap = new HashMap<>();
|
Map<String, ObjectNode> uniqueFontsMap = new HashMap<>();
|
||||||
@@ -526,13 +503,13 @@ public class GetInfoOnPDF {
|
|||||||
fontNode.put("IsNonsymbolic", (flags & 32) != 0);
|
fontNode.put("IsNonsymbolic", (flags & 32) != 0);
|
||||||
|
|
||||||
fontNode.put("FontFamily", fontDescriptor.getFontFamily());
|
fontNode.put("FontFamily", fontDescriptor.getFontFamily());
|
||||||
// Font stretch and BBox are not directly available in PDFBox's API, so these are omitted for simplicity
|
// Font stretch and BBox are not directly available in PDFBox's API, so
|
||||||
|
// these are omitted for simplicity
|
||||||
fontNode.put("FontWeight", fontDescriptor.getFontWeight());
|
fontNode.put("FontWeight", fontDescriptor.getFontWeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Create a unique key for this font node based on its attributes
|
// Create a unique key for this font node based on its attributes
|
||||||
String uniqueKey = fontNode.toString();
|
String uniqueKey = fontNode.toString();
|
||||||
|
|
||||||
// Increment count if this font exists, or initialize it if new
|
// Increment count if this font exists, or initialize it if new
|
||||||
if (uniqueFontsMap.containsKey(uniqueKey)) {
|
if (uniqueFontsMap.containsKey(uniqueKey)) {
|
||||||
@@ -551,17 +528,7 @@ public class GetInfoOnPDF {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pageInfo.set("Fonts", fontsArray);
|
pageInfo.set("Fonts", fontsArray);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Access resources dictionary
|
// Access resources dictionary
|
||||||
ArrayNode colorSpacesArray = objectMapper.createArrayNode();
|
ArrayNode colorSpacesArray = objectMapper.createArrayNode();
|
||||||
|
|
||||||
@@ -572,7 +539,7 @@ public class GetInfoOnPDF {
|
|||||||
PDICCBased iccBased = (PDICCBased) colorSpace;
|
PDICCBased iccBased = (PDICCBased) colorSpace;
|
||||||
PDStream iccData = iccBased.getPDStream();
|
PDStream iccData = iccBased.getPDStream();
|
||||||
byte[] iccBytes = iccData.toByteArray();
|
byte[] iccBytes = iccData.toByteArray();
|
||||||
|
|
||||||
// TODO: Further decode and analyze the ICC data if needed
|
// TODO: Further decode and analyze the ICC data if needed
|
||||||
ObjectNode iccProfileNode = objectMapper.createObjectNode();
|
ObjectNode iccProfileNode = objectMapper.createObjectNode();
|
||||||
iccProfileNode.put("ICC Profile Length", iccBytes.length);
|
iccProfileNode.put("ICC Profile Length", iccBytes.length);
|
||||||
@@ -580,14 +547,14 @@ public class GetInfoOnPDF {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pageInfo.set("Color Spaces & ICC Profiles", colorSpacesArray);
|
pageInfo.set("Color Spaces & ICC Profiles", colorSpacesArray);
|
||||||
|
|
||||||
|
|
||||||
// Other XObjects
|
// Other XObjects
|
||||||
Map<String, Integer> xObjectCountMap = new HashMap<>(); // To store the count for each type
|
Map<String, Integer> xObjectCountMap =
|
||||||
|
new HashMap<>(); // To store the count for each type
|
||||||
for (COSName name : resources.getXObjectNames()) {
|
for (COSName name : resources.getXObjectNames()) {
|
||||||
PDXObject xObject = resources.getXObject(name);
|
PDXObject xObject = resources.getXObject(name);
|
||||||
String xObjectType;
|
String xObjectType;
|
||||||
|
|
||||||
if (xObject instanceof PDImageXObject) {
|
if (xObject instanceof PDImageXObject) {
|
||||||
xObjectType = "Image";
|
xObjectType = "Image";
|
||||||
} else if (xObject instanceof PDFormXObject) {
|
} else if (xObject instanceof PDFormXObject) {
|
||||||
@@ -597,7 +564,8 @@ public class GetInfoOnPDF {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Increment the count for this type in the map
|
// Increment the count for this type in the map
|
||||||
xObjectCountMap.put(xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1);
|
xObjectCountMap.put(
|
||||||
|
xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the count map to pageInfo (or wherever you want to store it)
|
// Add the count map to pageInfo (or wherever you want to store it)
|
||||||
@@ -606,14 +574,11 @@ public class GetInfoOnPDF {
|
|||||||
xObjectCountNode.put(entry.getKey(), entry.getValue());
|
xObjectCountNode.put(entry.getKey(), entry.getValue());
|
||||||
}
|
}
|
||||||
pageInfo.set("XObjectCounts", xObjectCountNode);
|
pageInfo.set("XObjectCounts", xObjectCountNode);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ArrayNode multimediaArray = objectMapper.createArrayNode();
|
ArrayNode multimediaArray = objectMapper.createArrayNode();
|
||||||
|
|
||||||
for (PDAnnotation annotation : annotations) {
|
for (PDAnnotation annotation : annotations) {
|
||||||
if ("RichMedia".equals(annotation.getSubtype())) {
|
if ("RichMedia".equals(annotation.getSubtype())) {
|
||||||
ObjectNode multimediaNode = objectMapper.createObjectNode();
|
ObjectNode multimediaNode = objectMapper.createObjectNode();
|
||||||
// Extract details from the annotation as needed
|
// Extract details from the annotation as needed
|
||||||
multimediaArray.add(multimediaNode);
|
multimediaArray.add(multimediaNode);
|
||||||
@@ -622,32 +587,29 @@ public class GetInfoOnPDF {
|
|||||||
|
|
||||||
pageInfo.set("Multimedia", multimediaArray);
|
pageInfo.set("Multimedia", multimediaArray);
|
||||||
|
|
||||||
|
pageInfoParent.set("Page " + (pageNum + 1), pageInfo);
|
||||||
|
|
||||||
pageInfoParent.set("Page " + (pageNum+1), pageInfo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
jsonOutput.set("BasicInfo", basicInfo);
|
jsonOutput.set("BasicInfo", basicInfo);
|
||||||
jsonOutput.set("DocumentInfo", docInfoNode);
|
jsonOutput.set("DocumentInfo", docInfoNode);
|
||||||
jsonOutput.set("Compliancy", compliancy);
|
jsonOutput.set("Compliancy", compliancy);
|
||||||
jsonOutput.set("Encryption", encryption);
|
jsonOutput.set("Encryption", encryption);
|
||||||
jsonOutput.set("Other", other);
|
jsonOutput.set("Other", other);
|
||||||
jsonOutput.set("PerPageInfo", pageInfoParent);
|
jsonOutput.set("PerPageInfo", pageInfoParent);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Save JSON to file
|
// Save JSON to file
|
||||||
String jsonString = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonOutput);
|
String jsonString =
|
||||||
|
objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonOutput);
|
||||||
|
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
return WebResponseUtils.bytesToWebResponse(jsonString.getBytes(StandardCharsets.UTF_8), "response.json", MediaType.APPLICATION_JSON);
|
jsonString.getBytes(StandardCharsets.UTF_8),
|
||||||
|
"response.json",
|
||||||
|
MediaType.APPLICATION_JSON);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) {
|
private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) {
|
||||||
@@ -665,7 +627,7 @@ public class GetInfoOnPDF {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPageOrientation(double width, double height) {
|
public String getPageOrientation(double width, double height) {
|
||||||
if (width > height) {
|
if (width > height) {
|
||||||
return "Landscape";
|
return "Landscape";
|
||||||
} else if (height > width) {
|
} else if (height > width) {
|
||||||
@@ -674,6 +636,7 @@ public class GetInfoOnPDF {
|
|||||||
return "Square";
|
return "Square";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPageSize(float width, float height) {
|
public String getPageSize(float width, float height) {
|
||||||
// Define standard page sizes
|
// Define standard page sizes
|
||||||
Map<String, PDRectangle> standardSizes = new HashMap<>();
|
Map<String, PDRectangle> standardSizes = new HashMap<>();
|
||||||
@@ -696,21 +659,22 @@ public class GetInfoOnPDF {
|
|||||||
return "Custom";
|
return "Custom";
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isCloseToSize(float width, float height, float standardWidth, float standardHeight) {
|
private boolean isCloseToSize(
|
||||||
|
float width, float height, float standardWidth, float standardHeight) {
|
||||||
float tolerance = 1.0f; // You can adjust the tolerance as needed
|
float tolerance = 1.0f; // You can adjust the tolerance as needed
|
||||||
return Math.abs(width - standardWidth) <= tolerance && Math.abs(height - standardHeight) <= tolerance;
|
return Math.abs(width - standardWidth) <= tolerance
|
||||||
|
&& Math.abs(height - standardHeight) <= tolerance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ObjectNode getDimensionInfo(ObjectNode dimensionInfo, float width, float height) {
|
||||||
public ObjectNode getDimensionInfo(ObjectNode dimensionInfo, float width, float height) {
|
|
||||||
float ppi = 72; // Points Per Inch
|
float ppi = 72; // Points Per Inch
|
||||||
|
|
||||||
float widthInInches = width / ppi;
|
float widthInInches = width / ppi;
|
||||||
float heightInInches = height / ppi;
|
float heightInInches = height / ppi;
|
||||||
|
|
||||||
float widthInCm = widthInInches * 2.54f;
|
float widthInCm = widthInInches * 2.54f;
|
||||||
float heightInCm = heightInInches * 2.54f;
|
float heightInCm = heightInInches * 2.54f;
|
||||||
|
|
||||||
dimensionInfo.put("Width (px)", String.format("%.2f", width));
|
dimensionInfo.put("Width (px)", String.format("%.2f", width));
|
||||||
dimensionInfo.put("Height (px)", String.format("%.2f", height));
|
dimensionInfo.put("Height (px)", String.format("%.2f", height));
|
||||||
dimensionInfo.put("Width (in)", String.format("%.2f", widthInInches));
|
dimensionInfo.put("Width (in)", String.format("%.2f", widthInInches));
|
||||||
@@ -720,33 +684,33 @@ public class GetInfoOnPDF {
|
|||||||
return dimensionInfo;
|
return dimensionInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean checkForStandard(PDDocument document, String standardKeyword) {
|
||||||
|
// Check XMP Metadata
|
||||||
|
try {
|
||||||
|
PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata();
|
||||||
|
if (pdMetadata != null) {
|
||||||
|
COSInputStream metaStream = pdMetadata.createInputStream();
|
||||||
|
DomXmpParser domXmpParser = new DomXmpParser();
|
||||||
|
XMPMetadata xmpMeta = domXmpParser.parse(metaStream);
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
new XmpSerializer().serialize(xmpMeta, baos, true);
|
||||||
|
String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8);
|
||||||
|
|
||||||
public static boolean checkForStandard(PDDocument document, String standardKeyword) {
|
if (xmpString.contains(standardKeyword)) {
|
||||||
// Check XMP Metadata
|
return true;
|
||||||
try {
|
}
|
||||||
PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata();
|
|
||||||
if (pdMetadata != null) {
|
|
||||||
COSInputStream metaStream = pdMetadata.createInputStream();
|
|
||||||
DomXmpParser domXmpParser = new DomXmpParser();
|
|
||||||
XMPMetadata xmpMeta = domXmpParser.parse(metaStream);
|
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
new XmpSerializer().serialize(xmpMeta, baos, true);
|
|
||||||
String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8);
|
|
||||||
|
|
||||||
if (xmpString.contains(standardKeyword)) {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
} catch (
|
||||||
|
Exception
|
||||||
|
e) { // Catching general exception for brevity, ideally you'd catch specific
|
||||||
|
// exceptions.
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
} catch (Exception e) { // Catching general exception for brevity, ideally you'd catch specific exceptions.
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public ArrayNode exploreStructureTree(List<Object> nodes) {
|
public ArrayNode exploreStructureTree(List<Object> nodes) {
|
||||||
ArrayNode elementsArray = objectMapper.createArrayNode();
|
ArrayNode elementsArray = objectMapper.createArrayNode();
|
||||||
if (nodes != null) {
|
if (nodes != null) {
|
||||||
@@ -773,7 +737,6 @@ public static boolean checkForStandard(PDDocument document, String standardKeywo
|
|||||||
return elementsArray;
|
return elementsArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public String getContent(PDStructureElement structureElement) {
|
public String getContent(PDStructureElement structureElement) {
|
||||||
StringBuilder contentBuilder = new StringBuilder();
|
StringBuilder contentBuilder = new StringBuilder();
|
||||||
|
|
||||||
@@ -790,8 +753,7 @@ public static boolean checkForStandard(PDDocument document, String standardKeywo
|
|||||||
|
|
||||||
return contentBuilder.toString();
|
return contentBuilder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private String formatDate(Calendar calendar) {
|
private String formatDate(Calendar calendar) {
|
||||||
if (calendar != null) {
|
if (calendar != null) {
|
||||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ 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.AddPasswordRequest;
|
import stirling.software.SPDF.model.api.security.AddPasswordRequest;
|
||||||
import stirling.software.SPDF.model.api.security.PDFPasswordRequest;
|
import stirling.software.SPDF.model.api.security.PDFPasswordRequest;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/security")
|
@RequestMapping("/api/v1/security")
|
||||||
@Tag(name = "Security", description = "Security APIs")
|
@Tag(name = "Security", description = "Security APIs")
|
||||||
@@ -26,29 +28,31 @@ public class PasswordController {
|
|||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(PasswordController.class);
|
private static final Logger logger = LoggerFactory.getLogger(PasswordController.class);
|
||||||
|
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-password")
|
@PostMapping(consumes = "multipart/form-data", value = "/remove-password")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Remove password from a PDF file",
|
summary = "Remove password from a PDF file",
|
||||||
description = "This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO"
|
description =
|
||||||
)
|
"This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO")
|
||||||
public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request) throws IOException {
|
public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request)
|
||||||
|
throws IOException {
|
||||||
MultipartFile fileInput = request.getFileInput();
|
MultipartFile fileInput = request.getFileInput();
|
||||||
String password = request.getPassword();
|
String password = request.getPassword();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
PDDocument document = PDDocument.load(fileInput.getBytes(), password);
|
PDDocument document = PDDocument.load(fileInput.getBytes(), password);
|
||||||
document.setAllSecurityToBeRemoved(true);
|
document.setAllSecurityToBeRemoved(true);
|
||||||
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_password_removed.pdf");
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
|
document,
|
||||||
|
fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_password_removed.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/add-password")
|
@PostMapping(consumes = "multipart/form-data", value = "/add-password")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Add password to a PDF file",
|
summary = "Add password to a PDF file",
|
||||||
description = "This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF"
|
description =
|
||||||
)
|
"This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF")
|
||||||
public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request) throws IOException {
|
public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request)
|
||||||
|
throws IOException {
|
||||||
MultipartFile fileInput = request.getFileInput();
|
MultipartFile fileInput = request.getFileInput();
|
||||||
String ownerPassword = request.getOwnerPassword();
|
String ownerPassword = request.getOwnerPassword();
|
||||||
String password = request.getPassword();
|
String password = request.getPassword();
|
||||||
@@ -74,16 +78,19 @@ public class PasswordController {
|
|||||||
ap.setCanPrintFaithful(!canPrintFaithful);
|
ap.setCanPrintFaithful(!canPrintFaithful);
|
||||||
StandardProtectionPolicy spp = new StandardProtectionPolicy(ownerPassword, password, ap);
|
StandardProtectionPolicy spp = new StandardProtectionPolicy(ownerPassword, password, ap);
|
||||||
|
|
||||||
if(!"".equals(ownerPassword) || !"".equals(password)) {
|
if (!"".equals(ownerPassword) || !"".equals(password)) {
|
||||||
spp.setEncryptionKeyLength(keyLength);
|
spp.setEncryptionKeyLength(keyLength);
|
||||||
}
|
}
|
||||||
spp.setPermissions(ap);
|
spp.setPermissions(ap);
|
||||||
document.protect(spp);
|
document.protect(spp);
|
||||||
|
|
||||||
if("".equals(ownerPassword) && "".equals(password))
|
if ("".equals(ownerPassword) && "".equals(password))
|
||||||
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_permissions.pdf");
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf");
|
document,
|
||||||
|
fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_permissions.pdf");
|
||||||
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
|
document,
|
||||||
|
fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,12 @@ 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.PDFText;
|
import stirling.software.SPDF.model.PDFText;
|
||||||
import stirling.software.SPDF.model.api.security.RedactPdfRequest;
|
import stirling.software.SPDF.model.api.security.RedactPdfRequest;
|
||||||
import stirling.software.SPDF.pdf.TextFinder;
|
import stirling.software.SPDF.pdf.TextFinder;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/security")
|
@RequestMapping("/api/v1/security")
|
||||||
@Tag(name = "Security", description = "Security APIs")
|
@Tag(name = "Security", description = "Security APIs")
|
||||||
@@ -37,11 +39,13 @@ public class RedactController {
|
|||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(RedactController.class);
|
private static final Logger logger = LoggerFactory.getLogger(RedactController.class);
|
||||||
|
|
||||||
|
|
||||||
@PostMapping(value = "/auto-redact", consumes = "multipart/form-data")
|
@PostMapping(value = "/auto-redact", consumes = "multipart/form-data")
|
||||||
@Operation(summary = "Redacts listOfText in a PDF document",
|
@Operation(
|
||||||
description = "This operation takes an input PDF file and redacts the provided listOfText. Input:PDF, Output:PDF, Type:SISO")
|
summary = "Redacts listOfText in a PDF document",
|
||||||
public ResponseEntity<byte[]> redactPdf(@ModelAttribute RedactPdfRequest request) throws Exception {
|
description =
|
||||||
|
"This operation takes an input PDF file and redacts the provided listOfText. Input:PDF, Output:PDF, Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> redactPdf(@ModelAttribute RedactPdfRequest request)
|
||||||
|
throws Exception {
|
||||||
MultipartFile file = request.getFileInput();
|
MultipartFile file = request.getFileInput();
|
||||||
String listOfTextString = request.getListOfText();
|
String listOfTextString = request.getListOfText();
|
||||||
boolean useRegex = request.isUseRegex();
|
boolean useRegex = request.isUseRegex();
|
||||||
@@ -49,15 +53,15 @@ public class RedactController {
|
|||||||
String colorString = request.getRedactColor();
|
String colorString = request.getRedactColor();
|
||||||
float customPadding = request.getCustomPadding();
|
float customPadding = request.getCustomPadding();
|
||||||
boolean convertPDFToImage = request.isConvertPDFToImage();
|
boolean convertPDFToImage = request.isConvertPDFToImage();
|
||||||
|
|
||||||
System.out.println(listOfTextString);
|
System.out.println(listOfTextString);
|
||||||
String[] listOfText = listOfTextString.split("\n");
|
String[] listOfText = listOfTextString.split("\n");
|
||||||
byte[] bytes = file.getBytes();
|
byte[] bytes = file.getBytes();
|
||||||
PDDocument document = PDDocument.load(new ByteArrayInputStream(bytes));
|
PDDocument document = PDDocument.load(new ByteArrayInputStream(bytes));
|
||||||
|
|
||||||
Color redactColor;
|
Color redactColor;
|
||||||
try {
|
try {
|
||||||
if (!colorString.startsWith("#")) {
|
if (!colorString.startsWith("#")) {
|
||||||
colorString = "#" + colorString;
|
colorString = "#" + colorString;
|
||||||
}
|
}
|
||||||
redactColor = Color.decode(colorString);
|
redactColor = Color.decode(colorString);
|
||||||
@@ -66,18 +70,14 @@ public class RedactController {
|
|||||||
redactColor = Color.BLACK;
|
redactColor = Color.BLACK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
for (String text : listOfText) {
|
for (String text : listOfText) {
|
||||||
text = text.trim();
|
text = text.trim();
|
||||||
System.out.println(text);
|
System.out.println(text);
|
||||||
TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool);
|
TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool);
|
||||||
List<PDFText> foundTexts = textFinder.getTextLocations(document);
|
List<PDFText> foundTexts = textFinder.getTextLocations(document);
|
||||||
redactFoundText(document, foundTexts, customPadding,redactColor);
|
redactFoundText(document, foundTexts, customPadding, redactColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (convertPDFToImage) {
|
if (convertPDFToImage) {
|
||||||
PDDocument imageDocument = new PDDocument();
|
PDDocument imageDocument = new PDDocument();
|
||||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
@@ -97,27 +97,33 @@ public class RedactController {
|
|||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
document.save(baos);
|
document.save(baos);
|
||||||
document.close();
|
document.close();
|
||||||
|
|
||||||
byte[] pdfContent = baos.toByteArray();
|
byte[] pdfContent = baos.toByteArray();
|
||||||
return WebResponseUtils.bytesToWebResponse(pdfContent,
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
pdfContent,
|
||||||
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_redacted.pdf");
|
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_redacted.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void redactFoundText(
|
||||||
private void redactFoundText(PDDocument document, List<PDFText> blocks, float customPadding, Color redactColor) throws IOException {
|
PDDocument document, List<PDFText> blocks, float customPadding, Color redactColor)
|
||||||
|
throws IOException {
|
||||||
var allPages = document.getDocumentCatalog().getPages();
|
var allPages = document.getDocumentCatalog().getPages();
|
||||||
|
|
||||||
for (PDFText block : blocks) {
|
for (PDFText block : blocks) {
|
||||||
var page = allPages.get(block.getPageIndex());
|
var page = allPages.get(block.getPageIndex());
|
||||||
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true, true);
|
PDPageContentStream contentStream =
|
||||||
|
new PDPageContentStream(
|
||||||
|
document, page, PDPageContentStream.AppendMode.APPEND, true, true);
|
||||||
contentStream.setNonStrokingColor(redactColor);
|
contentStream.setNonStrokingColor(redactColor);
|
||||||
float padding = (block.getY2() - block.getY1()) * 0.3f + customPadding;
|
float padding = (block.getY2() - block.getY1()) * 0.3f + customPadding;
|
||||||
PDRectangle pageBox = page.getBBox();
|
PDRectangle pageBox = page.getBBox();
|
||||||
contentStream.addRect(block.getX1(), pageBox.getHeight() - block.getY1() - padding, block.getX2() - block.getX1(), block.getY2() - block.getY1() + 2 * padding);
|
contentStream.addRect(
|
||||||
|
block.getX1(),
|
||||||
|
pageBox.getHeight() - block.getY1() - padding,
|
||||||
|
block.getX2() - block.getX1(),
|
||||||
|
block.getY2() - block.getY1() + 2 * padding);
|
||||||
contentStream.fill();
|
contentStream.fill();
|
||||||
contentStream.close();
|
contentStream.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
package stirling.software.SPDF.controller.api.security;
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.apache.pdfbox.cos.COSDictionary;
|
import org.apache.pdfbox.cos.COSDictionary;
|
||||||
@@ -28,6 +29,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.SanitizePdfRequest;
|
import stirling.software.SPDF.model.api.security.SanitizePdfRequest;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@@ -36,59 +38,68 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@Tag(name = "Security", description = "Security APIs")
|
@Tag(name = "Security", description = "Security APIs")
|
||||||
public class SanitizeController {
|
public class SanitizeController {
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf")
|
||||||
@Operation(summary = "Sanitize a PDF file",
|
@Operation(
|
||||||
description = "This endpoint processes a PDF file and removes specific elements based on the provided options. Input:PDF Output:PDF Type:SISO")
|
summary = "Sanitize a PDF file",
|
||||||
public ResponseEntity<byte[]> sanitizePDF(@ModelAttribute SanitizePdfRequest request) throws IOException {
|
description =
|
||||||
MultipartFile inputFile = request.getFileInput();
|
"This endpoint processes a PDF file and removes specific elements based on the provided options. Input:PDF Output:PDF Type:SISO")
|
||||||
boolean removeJavaScript = request.isRemoveJavaScript();
|
public ResponseEntity<byte[]> sanitizePDF(@ModelAttribute SanitizePdfRequest request)
|
||||||
boolean removeEmbeddedFiles = request.isRemoveEmbeddedFiles();
|
throws IOException {
|
||||||
boolean removeMetadata = request.isRemoveMetadata();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
boolean removeLinks = request.isRemoveLinks();
|
boolean removeJavaScript = request.isRemoveJavaScript();
|
||||||
boolean removeFonts = request.isRemoveFonts();
|
boolean removeEmbeddedFiles = request.isRemoveEmbeddedFiles();
|
||||||
|
boolean removeMetadata = request.isRemoveMetadata();
|
||||||
|
boolean removeLinks = request.isRemoveLinks();
|
||||||
|
boolean removeFonts = request.isRemoveFonts();
|
||||||
|
|
||||||
try (PDDocument document = PDDocument.load(inputFile.getInputStream())) {
|
try (PDDocument document = PDDocument.load(inputFile.getInputStream())) {
|
||||||
if (removeJavaScript) {
|
if (removeJavaScript) {
|
||||||
sanitizeJavaScript(document);
|
sanitizeJavaScript(document);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removeEmbeddedFiles) {
|
if (removeEmbeddedFiles) {
|
||||||
sanitizeEmbeddedFiles(document);
|
sanitizeEmbeddedFiles(document);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removeMetadata) {
|
if (removeMetadata) {
|
||||||
sanitizeMetadata(document);
|
sanitizeMetadata(document);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removeLinks) {
|
if (removeLinks) {
|
||||||
sanitizeLinks(document);
|
sanitizeLinks(document);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removeFonts) {
|
if (removeFonts) {
|
||||||
sanitizeFonts(document);
|
sanitizeFonts(document);
|
||||||
}
|
}
|
||||||
|
|
||||||
return WebResponseUtils.pdfDocToWebResponse(document, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_sanitized.pdf");
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
}
|
document,
|
||||||
}
|
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||||
private void sanitizeJavaScript(PDDocument document) throws IOException {
|
+ "_sanitized.pdf");
|
||||||
// Get the root dictionary (catalog) of the PDF
|
}
|
||||||
PDDocumentCatalog catalog = document.getDocumentCatalog();
|
}
|
||||||
|
|
||||||
// Get the Names dictionary
|
private void sanitizeJavaScript(PDDocument document) throws IOException {
|
||||||
COSDictionary namesDict = (COSDictionary) catalog.getCOSObject().getDictionaryObject(COSName.NAMES);
|
// Get the root dictionary (catalog) of the PDF
|
||||||
|
PDDocumentCatalog catalog = document.getDocumentCatalog();
|
||||||
|
|
||||||
if (namesDict != null) {
|
// Get the Names dictionary
|
||||||
// Get the JavaScript dictionary
|
COSDictionary namesDict =
|
||||||
COSDictionary javaScriptDict = (COSDictionary) namesDict.getDictionaryObject(COSName.getPDFName("JavaScript"));
|
(COSDictionary) catalog.getCOSObject().getDictionaryObject(COSName.NAMES);
|
||||||
|
|
||||||
if (javaScriptDict != null) {
|
if (namesDict != null) {
|
||||||
// Remove the JavaScript dictionary
|
// Get the JavaScript dictionary
|
||||||
namesDict.removeItem(COSName.getPDFName("JavaScript"));
|
COSDictionary javaScriptDict =
|
||||||
}
|
(COSDictionary) namesDict.getDictionaryObject(COSName.getPDFName("JavaScript"));
|
||||||
}
|
|
||||||
|
if (javaScriptDict != null) {
|
||||||
for (PDPage page : document.getPages()) {
|
// Remove the JavaScript dictionary
|
||||||
|
namesDict.removeItem(COSName.getPDFName("JavaScript"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (PDPage page : document.getPages()) {
|
||||||
for (PDAnnotation annotation : page.getAnnotations()) {
|
for (PDAnnotation annotation : page.getAnnotations()) {
|
||||||
if (annotation instanceof PDAnnotationWidget) {
|
if (annotation instanceof PDAnnotationWidget) {
|
||||||
PDAnnotationWidget widget = (PDAnnotationWidget) annotation;
|
PDAnnotationWidget widget = (PDAnnotationWidget) annotation;
|
||||||
@@ -96,33 +107,30 @@ public class SanitizeController {
|
|||||||
if (action instanceof PDActionJavaScript) {
|
if (action instanceof PDActionJavaScript) {
|
||||||
widget.setAction(null);
|
widget.setAction(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
|
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
|
||||||
if (acroForm != null) {
|
if (acroForm != null) {
|
||||||
for (PDField field : acroForm.getFields()) {
|
for (PDField field : acroForm.getFields()) {
|
||||||
PDFormFieldAdditionalActions actions = field.getActions();
|
PDFormFieldAdditionalActions actions = field.getActions();
|
||||||
if(actions != null) {
|
if (actions != null) {
|
||||||
if (actions.getC() instanceof PDActionJavaScript) {
|
if (actions.getC() instanceof PDActionJavaScript) {
|
||||||
actions.setC(null);
|
actions.setC(null);
|
||||||
}
|
}
|
||||||
if (actions.getF() instanceof PDActionJavaScript) {
|
if (actions.getF() instanceof PDActionJavaScript) {
|
||||||
actions.setF(null);
|
actions.setF(null);
|
||||||
}
|
}
|
||||||
if (actions.getK() instanceof PDActionJavaScript) {
|
if (actions.getK() instanceof PDActionJavaScript) {
|
||||||
actions.setK(null);
|
actions.setK(null);
|
||||||
}
|
}
|
||||||
if (actions.getV() instanceof PDActionJavaScript) {
|
if (actions.getV() instanceof PDActionJavaScript) {
|
||||||
actions.setV(null);
|
actions.setV(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void sanitizeEmbeddedFiles(PDDocument document) {
|
private void sanitizeEmbeddedFiles(PDDocument document) {
|
||||||
PDPageTree allPages = document.getPages();
|
PDPageTree allPages = document.getPages();
|
||||||
@@ -134,7 +142,6 @@ public class SanitizeController {
|
|||||||
res.getCOSObject().removeItem(COSName.getPDFName("EmbeddedFiles"));
|
res.getCOSObject().removeItem(COSName.getPDFName("EmbeddedFiles"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void sanitizeMetadata(PDDocument document) {
|
private void sanitizeMetadata(PDDocument document) {
|
||||||
PDMetadata metadata = document.getDocumentCatalog().getMetadata();
|
PDMetadata metadata = document.getDocumentCatalog().getMetadata();
|
||||||
@@ -143,8 +150,6 @@ public class SanitizeController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void sanitizeLinks(PDDocument document) throws IOException {
|
private void sanitizeLinks(PDDocument document) throws IOException {
|
||||||
for (PDPage page : document.getPages()) {
|
for (PDPage page : document.getPages()) {
|
||||||
for (PDAnnotation annotation : page.getAnnotations()) {
|
for (PDAnnotation annotation : page.getAnnotations()) {
|
||||||
@@ -163,5 +168,4 @@ public class SanitizeController {
|
|||||||
page.getResources().getCOSObject().removeItem(COSName.getPDFName("Font"));
|
page.getResources().getCOSObject().removeItem(COSName.getPDFName("Font"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.security.AddWatermarkRequest;
|
import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@@ -38,154 +39,198 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@Tag(name = "Security", description = "Security APIs")
|
@Tag(name = "Security", description = "Security APIs")
|
||||||
public class WatermarkController {
|
public class WatermarkController {
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/add-watermark")
|
@PostMapping(consumes = "multipart/form-data", value = "/add-watermark")
|
||||||
@Operation(summary = "Add watermark to a PDF file", description = "This endpoint adds a watermark to a given PDF file. Users can specify the watermark type (text or image), rotation, opacity, width spacer, and height spacer. Input:PDF Output:PDF Type:SISO")
|
@Operation(
|
||||||
public ResponseEntity<byte[]> addWatermark(@ModelAttribute AddWatermarkRequest request) throws IOException, Exception {
|
summary = "Add watermark to a PDF file",
|
||||||
MultipartFile pdfFile = request.getFileInput();
|
description =
|
||||||
String watermarkType = request.getWatermarkType();
|
"This endpoint adds a watermark to a given PDF file. Users can specify the watermark type (text or image), rotation, opacity, width spacer, and height spacer. Input:PDF Output:PDF Type:SISO")
|
||||||
String watermarkText = request.getWatermarkText();
|
public ResponseEntity<byte[]> addWatermark(@ModelAttribute AddWatermarkRequest request)
|
||||||
MultipartFile watermarkImage = request.getWatermarkImage();
|
throws IOException, Exception {
|
||||||
String alphabet = request.getAlphabet();
|
MultipartFile pdfFile = request.getFileInput();
|
||||||
float fontSize = request.getFontSize();
|
String watermarkType = request.getWatermarkType();
|
||||||
float rotation = request.getRotation();
|
String watermarkText = request.getWatermarkText();
|
||||||
float opacity = request.getOpacity();
|
MultipartFile watermarkImage = request.getWatermarkImage();
|
||||||
int widthSpacer = request.getWidthSpacer();
|
String alphabet = request.getAlphabet();
|
||||||
int heightSpacer = request.getHeightSpacer();
|
float fontSize = request.getFontSize();
|
||||||
|
float rotation = request.getRotation();
|
||||||
|
float opacity = request.getOpacity();
|
||||||
|
int widthSpacer = request.getWidthSpacer();
|
||||||
|
int heightSpacer = request.getHeightSpacer();
|
||||||
|
|
||||||
// Load the input PDF
|
// Load the input PDF
|
||||||
PDDocument document = PDDocument.load(pdfFile.getInputStream());
|
PDDocument document = PDDocument.load(pdfFile.getInputStream());
|
||||||
|
|
||||||
// Create a page in the document
|
// Create a page in the document
|
||||||
for (PDPage page : document.getPages()) {
|
for (PDPage page : document.getPages()) {
|
||||||
|
|
||||||
// Get the page's content stream
|
// Get the page's content stream
|
||||||
PDPageContentStream contentStream = new PDPageContentStream(document, page,
|
PDPageContentStream contentStream =
|
||||||
PDPageContentStream.AppendMode.APPEND, true);
|
new PDPageContentStream(
|
||||||
|
document, page, PDPageContentStream.AppendMode.APPEND, true);
|
||||||
|
|
||||||
// Set transparency
|
// Set transparency
|
||||||
PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
|
PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
|
||||||
graphicsState.setNonStrokingAlphaConstant(opacity);
|
graphicsState.setNonStrokingAlphaConstant(opacity);
|
||||||
contentStream.setGraphicsStateParameters(graphicsState);
|
contentStream.setGraphicsStateParameters(graphicsState);
|
||||||
|
|
||||||
if (watermarkType.equalsIgnoreCase("text")) {
|
if (watermarkType.equalsIgnoreCase("text")) {
|
||||||
addTextWatermark(contentStream, watermarkText, document, page, rotation, widthSpacer, heightSpacer,
|
addTextWatermark(
|
||||||
fontSize, alphabet);
|
contentStream,
|
||||||
} else if (watermarkType.equalsIgnoreCase("image")) {
|
watermarkText,
|
||||||
addImageWatermark(contentStream, watermarkImage, document, page, rotation, widthSpacer, heightSpacer,
|
document,
|
||||||
fontSize);
|
page,
|
||||||
}
|
rotation,
|
||||||
|
widthSpacer,
|
||||||
|
heightSpacer,
|
||||||
|
fontSize,
|
||||||
|
alphabet);
|
||||||
|
} else if (watermarkType.equalsIgnoreCase("image")) {
|
||||||
|
addImageWatermark(
|
||||||
|
contentStream,
|
||||||
|
watermarkImage,
|
||||||
|
document,
|
||||||
|
page,
|
||||||
|
rotation,
|
||||||
|
widthSpacer,
|
||||||
|
heightSpacer,
|
||||||
|
fontSize);
|
||||||
|
}
|
||||||
|
|
||||||
// Close the content stream
|
// Close the content stream
|
||||||
contentStream.close();
|
contentStream.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
return WebResponseUtils.pdfDocToWebResponse(document,
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf");
|
document,
|
||||||
}
|
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf");
|
||||||
|
}
|
||||||
|
|
||||||
private void addTextWatermark(PDPageContentStream contentStream, String watermarkText, PDDocument document,
|
private void addTextWatermark(
|
||||||
PDPage page, float rotation, int widthSpacer, int heightSpacer, float fontSize, String alphabet) throws IOException {
|
PDPageContentStream contentStream,
|
||||||
String resourceDir = "";
|
String watermarkText,
|
||||||
PDFont font = PDType1Font.HELVETICA_BOLD;
|
PDDocument document,
|
||||||
switch (alphabet) {
|
PDPage page,
|
||||||
case "arabic":
|
float rotation,
|
||||||
resourceDir = "static/fonts/NotoSansArabic-Regular.ttf";
|
int widthSpacer,
|
||||||
break;
|
int heightSpacer,
|
||||||
case "japanese":
|
float fontSize,
|
||||||
resourceDir = "static/fonts/Meiryo.ttf";
|
String alphabet)
|
||||||
break;
|
throws IOException {
|
||||||
case "korean":
|
String resourceDir = "";
|
||||||
resourceDir = "static/fonts/malgun.ttf";
|
PDFont font = PDType1Font.HELVETICA_BOLD;
|
||||||
break;
|
switch (alphabet) {
|
||||||
case "chinese":
|
case "arabic":
|
||||||
resourceDir = "static/fonts/SimSun.ttf";
|
resourceDir = "static/fonts/NotoSansArabic-Regular.ttf";
|
||||||
break;
|
break;
|
||||||
case "roman":
|
case "japanese":
|
||||||
default:
|
resourceDir = "static/fonts/Meiryo.ttf";
|
||||||
resourceDir = "static/fonts/NotoSans-Regular.ttf";
|
break;
|
||||||
break;
|
case "korean":
|
||||||
}
|
resourceDir = "static/fonts/malgun.ttf";
|
||||||
|
break;
|
||||||
|
case "chinese":
|
||||||
|
resourceDir = "static/fonts/SimSun.ttf";
|
||||||
|
break;
|
||||||
|
case "roman":
|
||||||
|
default:
|
||||||
|
resourceDir = "static/fonts/NotoSans-Regular.ttf";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resourceDir.equals("")) {
|
||||||
if(!resourceDir.equals("")) {
|
|
||||||
ClassPathResource classPathResource = new ClassPathResource(resourceDir);
|
ClassPathResource classPathResource = new ClassPathResource(resourceDir);
|
||||||
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
|
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
|
||||||
File tempFile = File.createTempFile("NotoSansFont", fileExtension);
|
File tempFile = File.createTempFile("NotoSansFont", fileExtension);
|
||||||
try (InputStream is = classPathResource.getInputStream(); FileOutputStream os = new FileOutputStream(tempFile)) {
|
try (InputStream is = classPathResource.getInputStream();
|
||||||
|
FileOutputStream os = new FileOutputStream(tempFile)) {
|
||||||
IOUtils.copy(is, os);
|
IOUtils.copy(is, os);
|
||||||
}
|
}
|
||||||
|
|
||||||
font = PDType0Font.load(document, tempFile);
|
font = PDType0Font.load(document, tempFile);
|
||||||
tempFile.deleteOnExit();
|
tempFile.deleteOnExit();
|
||||||
}
|
}
|
||||||
|
|
||||||
contentStream.setFont(font, fontSize);
|
|
||||||
contentStream.setNonStrokingColor(Color.LIGHT_GRAY);
|
|
||||||
|
|
||||||
// Set size and location of text watermark
|
contentStream.setFont(font, fontSize);
|
||||||
float watermarkWidth = widthSpacer + font.getStringWidth(watermarkText) * fontSize / 1000;
|
contentStream.setNonStrokingColor(Color.LIGHT_GRAY);
|
||||||
float watermarkHeight = heightSpacer + fontSize;
|
|
||||||
float pageWidth = page.getMediaBox().getWidth();
|
|
||||||
float pageHeight = page.getMediaBox().getHeight();
|
|
||||||
int watermarkRows = (int) (pageHeight / watermarkHeight + 1);
|
|
||||||
int watermarkCols = (int) (pageWidth / watermarkWidth + 1);
|
|
||||||
|
|
||||||
// Add the text watermark
|
// Set size and location of text watermark
|
||||||
for (int i = 0; i < watermarkRows; i++) {
|
float watermarkWidth = widthSpacer + font.getStringWidth(watermarkText) * fontSize / 1000;
|
||||||
for (int j = 0; j < watermarkCols; j++) {
|
float watermarkHeight = heightSpacer + fontSize;
|
||||||
contentStream.beginText();
|
float pageWidth = page.getMediaBox().getWidth();
|
||||||
contentStream.setTextMatrix(Matrix.getRotateInstance((float) Math.toRadians(rotation),
|
float pageHeight = page.getMediaBox().getHeight();
|
||||||
j * watermarkWidth, i * watermarkHeight));
|
int watermarkRows = (int) (pageHeight / watermarkHeight + 1);
|
||||||
contentStream.showText(watermarkText);
|
int watermarkCols = (int) (pageWidth / watermarkWidth + 1);
|
||||||
contentStream.endText();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addImageWatermark(PDPageContentStream contentStream, MultipartFile watermarkImage, PDDocument document, PDPage page, float rotation,
|
// Add the text watermark
|
||||||
int widthSpacer, int heightSpacer, float fontSize) throws IOException {
|
for (int i = 0; i < watermarkRows; i++) {
|
||||||
|
for (int j = 0; j < watermarkCols; j++) {
|
||||||
|
contentStream.beginText();
|
||||||
|
contentStream.setTextMatrix(
|
||||||
|
Matrix.getRotateInstance(
|
||||||
|
(float) Math.toRadians(rotation),
|
||||||
|
j * watermarkWidth,
|
||||||
|
i * watermarkHeight));
|
||||||
|
contentStream.showText(watermarkText);
|
||||||
|
contentStream.endText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load the watermark image
|
private void addImageWatermark(
|
||||||
BufferedImage image = ImageIO.read(watermarkImage.getInputStream());
|
PDPageContentStream contentStream,
|
||||||
|
MultipartFile watermarkImage,
|
||||||
|
PDDocument document,
|
||||||
|
PDPage page,
|
||||||
|
float rotation,
|
||||||
|
int widthSpacer,
|
||||||
|
int heightSpacer,
|
||||||
|
float fontSize)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
// Compute width based on original aspect ratio
|
// Load the watermark image
|
||||||
float aspectRatio = (float) image.getWidth() / (float) image.getHeight();
|
BufferedImage image = ImageIO.read(watermarkImage.getInputStream());
|
||||||
|
|
||||||
// Desired physical height (in PDF points)
|
// Compute width based on original aspect ratio
|
||||||
float desiredPhysicalHeight = fontSize ;
|
float aspectRatio = (float) image.getWidth() / (float) image.getHeight();
|
||||||
|
|
||||||
// Desired physical width based on the aspect ratio
|
// Desired physical height (in PDF points)
|
||||||
float desiredPhysicalWidth = desiredPhysicalHeight * aspectRatio;
|
float desiredPhysicalHeight = fontSize;
|
||||||
|
|
||||||
// Convert the BufferedImage to PDImageXObject
|
// Desired physical width based on the aspect ratio
|
||||||
PDImageXObject xobject = LosslessFactory.createFromImage(document, image);
|
float desiredPhysicalWidth = desiredPhysicalHeight * aspectRatio;
|
||||||
|
|
||||||
// Calculate the number of rows and columns for watermarks
|
// Convert the BufferedImage to PDImageXObject
|
||||||
float pageWidth = page.getMediaBox().getWidth();
|
PDImageXObject xobject = LosslessFactory.createFromImage(document, image);
|
||||||
float pageHeight = page.getMediaBox().getHeight();
|
|
||||||
int watermarkRows = (int) ((pageHeight + heightSpacer) / (desiredPhysicalHeight + heightSpacer));
|
|
||||||
int watermarkCols = (int) ((pageWidth + widthSpacer) / (desiredPhysicalWidth + widthSpacer));
|
|
||||||
|
|
||||||
for (int i = 0; i < watermarkRows; i++) {
|
// Calculate the number of rows and columns for watermarks
|
||||||
for (int j = 0; j < watermarkCols; j++) {
|
float pageWidth = page.getMediaBox().getWidth();
|
||||||
float x = j * (desiredPhysicalWidth + widthSpacer);
|
float pageHeight = page.getMediaBox().getHeight();
|
||||||
float y = i * (desiredPhysicalHeight + heightSpacer);
|
int watermarkRows =
|
||||||
|
(int) ((pageHeight + heightSpacer) / (desiredPhysicalHeight + heightSpacer));
|
||||||
|
int watermarkCols =
|
||||||
|
(int) ((pageWidth + widthSpacer) / (desiredPhysicalWidth + widthSpacer));
|
||||||
|
|
||||||
// Save the graphics state
|
for (int i = 0; i < watermarkRows; i++) {
|
||||||
contentStream.saveGraphicsState();
|
for (int j = 0; j < watermarkCols; j++) {
|
||||||
|
float x = j * (desiredPhysicalWidth + widthSpacer);
|
||||||
|
float y = i * (desiredPhysicalHeight + heightSpacer);
|
||||||
|
|
||||||
// Create rotation matrix and rotate
|
// Save the graphics state
|
||||||
contentStream.transform(Matrix.getTranslateInstance(x + desiredPhysicalWidth / 2, y + desiredPhysicalHeight / 2));
|
contentStream.saveGraphicsState();
|
||||||
contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0));
|
|
||||||
contentStream.transform(Matrix.getTranslateInstance(-desiredPhysicalWidth / 2, -desiredPhysicalHeight / 2));
|
|
||||||
|
|
||||||
// Draw the image and restore the graphics state
|
|
||||||
contentStream.drawImage(xobject, 0, 0, desiredPhysicalWidth, desiredPhysicalHeight);
|
|
||||||
contentStream.restoreGraphicsState();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Create rotation matrix and rotate
|
||||||
|
contentStream.transform(
|
||||||
|
Matrix.getTranslateInstance(
|
||||||
|
x + desiredPhysicalWidth / 2, y + desiredPhysicalHeight / 2));
|
||||||
|
contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0));
|
||||||
|
contentStream.transform(
|
||||||
|
Matrix.getTranslateInstance(
|
||||||
|
-desiredPhysicalWidth / 2, -desiredPhysicalHeight / 2));
|
||||||
|
|
||||||
|
// Draw the image and restore the graphics state
|
||||||
|
contentStream.drawImage(xobject, 0, 0, desiredPhysicalWidth, desiredPhysicalHeight);
|
||||||
|
contentStream.restoreGraphicsState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
package stirling.software.SPDF.controller.api.strippers;
|
package stirling.software.SPDF.controller.api.strippers;
|
||||||
|
|
||||||
|
import java.awt.Shape;
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
|
import java.awt.geom.Rectangle2D;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import org.apache.fontbox.util.BoundingBox;
|
import org.apache.fontbox.util.BoundingBox;
|
||||||
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;
|
||||||
@@ -9,102 +23,80 @@ import org.apache.pdfbox.text.PDFTextStripper;
|
|||||||
import org.apache.pdfbox.text.PDFTextStripperByArea;
|
import org.apache.pdfbox.text.PDFTextStripperByArea;
|
||||||
import org.apache.pdfbox.text.TextPosition;
|
import org.apache.pdfbox.text.TextPosition;
|
||||||
|
|
||||||
import java.awt.*;
|
|
||||||
import java.awt.geom.AffineTransform;
|
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.io.Writer;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Class to extract tabular data from a PDF. Works by making a first pass of the page to group all
|
||||||
|
* nearby text items together, and then inferring a 2D grid from these regions. Each table cell is
|
||||||
|
* then extracted using a PDFTextStripperByArea object.
|
||||||
*
|
*
|
||||||
* Class to extract tabular data from a PDF.
|
* <p>Works best when headers are included in the detected region, to ensure representative text in
|
||||||
* Works by making a first pass of the page to group all nearby text items
|
* every column.
|
||||||
* together, and then inferring a 2D grid from these regions. Each table cell
|
|
||||||
* is then extracted using a PDFTextStripperByArea object.
|
|
||||||
*
|
*
|
||||||
* Works best when
|
* <p>Based upon DrawPrintTextLocations PDFBox example
|
||||||
* headers are included in the detected region, to ensure representative text
|
|
||||||
* in every column.
|
|
||||||
*
|
|
||||||
* Based upon DrawPrintTextLocations PDFBox example
|
|
||||||
* (https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/org/apache/pdfbox/examples/util/DrawPrintTextLocations.java)
|
* (https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/org/apache/pdfbox/examples/util/DrawPrintTextLocations.java)
|
||||||
*
|
*
|
||||||
* @author Beldaz
|
* @author Beldaz
|
||||||
*/
|
*/
|
||||||
public class PDFTableStripper extends PDFTextStripper
|
public class PDFTableStripper extends PDFTextStripper {
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This will print the documents data, for each table cell.
|
* This will print the documents data, for each table cell.
|
||||||
*
|
*
|
||||||
* @param args The command line arguments.
|
* @param args The command line arguments.
|
||||||
*
|
|
||||||
* @throws IOException If there is an error parsing the document.
|
* @throws IOException If there is an error parsing the document.
|
||||||
*/
|
*/
|
||||||
/*
|
/*
|
||||||
* Used in methods derived from DrawPrintTextLocations
|
* Used in methods derived from DrawPrintTextLocations
|
||||||
*/
|
*/
|
||||||
private AffineTransform flipAT;
|
private AffineTransform flipAT;
|
||||||
|
|
||||||
private AffineTransform rotateAT;
|
private AffineTransform rotateAT;
|
||||||
|
|
||||||
/**
|
/** Regions updated by calls to writeString */
|
||||||
* Regions updated by calls to writeString
|
|
||||||
*/
|
|
||||||
private Set<Rectangle2D> boxes;
|
private Set<Rectangle2D> boxes;
|
||||||
|
|
||||||
// Border to allow when finding intersections
|
// Border to allow when finding intersections
|
||||||
private double dx = 1.0; // This value works for me, feel free to tweak (or add setter)
|
private double dx = 1.0; // This value works for me, feel free to tweak (or add setter)
|
||||||
private double dy = 0.000; // Rows of text tend to overlap, so need to extend
|
private double dy = 0.000; // Rows of text tend to overlap, so need to extend
|
||||||
|
|
||||||
/**
|
/** Region in which to find table (otherwise whole page) */
|
||||||
* Region in which to find table (otherwise whole page)
|
|
||||||
*/
|
|
||||||
private Rectangle2D regionArea;
|
private Rectangle2D regionArea;
|
||||||
|
|
||||||
/**
|
/** Number of rows in inferred table */
|
||||||
* Number of rows in inferred table
|
private int nRows = 0;
|
||||||
*/
|
|
||||||
private int nRows=0;
|
|
||||||
|
|
||||||
/**
|
/** Number of columns in inferred table */
|
||||||
* Number of columns in inferred table
|
private int nCols = 0;
|
||||||
*/
|
|
||||||
private int nCols=0;
|
|
||||||
|
|
||||||
/**
|
/** This is the object that does the text extraction */
|
||||||
* This is the object that does the text extraction
|
|
||||||
*/
|
|
||||||
private PDFTextStripperByArea regionStripper;
|
private PDFTextStripperByArea regionStripper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1D intervals - used for calculateTableRegions()
|
* 1D intervals - used for calculateTableRegions()
|
||||||
* @author Beldaz
|
|
||||||
*
|
*
|
||||||
|
* @author Beldaz
|
||||||
*/
|
*/
|
||||||
public static class Interval {
|
public static class Interval {
|
||||||
double start;
|
double start;
|
||||||
double end;
|
double end;
|
||||||
|
|
||||||
public Interval(double start, double end) {
|
public Interval(double start, double end) {
|
||||||
this.start=start; this.end = end;
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void add(Interval col) {
|
public void add(Interval col) {
|
||||||
if(col.start<start)
|
if (col.start < start) start = col.start;
|
||||||
start = col.start;
|
if (col.end > end) end = col.end;
|
||||||
if(col.end>end)
|
|
||||||
end = col.end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void addTo(Interval x, LinkedList<Interval> columns) {
|
public static void addTo(Interval x, LinkedList<Interval> columns) {
|
||||||
int p = 0;
|
int p = 0;
|
||||||
Iterator<Interval> it = columns.iterator();
|
Iterator<Interval> it = columns.iterator();
|
||||||
// Find where x should go
|
// Find where x should go
|
||||||
while(it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
Interval col = it.next();
|
Interval col = it.next();
|
||||||
if(x.end>=col.start) {
|
if (x.end >= col.start) {
|
||||||
if(x.start<=col.end) { // overlaps
|
if (x.start <= col.end) { // overlaps
|
||||||
x.add(col);
|
x.add(col);
|
||||||
it.remove();
|
it.remove();
|
||||||
}
|
}
|
||||||
@@ -112,30 +104,26 @@ public class PDFTableStripper extends PDFTextStripper
|
|||||||
}
|
}
|
||||||
++p;
|
++p;
|
||||||
}
|
}
|
||||||
while(it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
Interval col = it.next();
|
Interval col = it.next();
|
||||||
if(x.start>col.end)
|
if (x.start > col.end) break;
|
||||||
break;
|
|
||||||
x.add(col);
|
x.add(col);
|
||||||
it.remove();
|
it.remove();
|
||||||
}
|
}
|
||||||
columns.add(p, x);
|
columns.add(p, x);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiate a new PDFTableStripper object.
|
* Instantiate a new PDFTableStripper object.
|
||||||
*
|
*
|
||||||
* @param document
|
* @param document
|
||||||
* @throws IOException If there is an error loading the properties.
|
* @throws IOException If there is an error loading the properties.
|
||||||
*/
|
*/
|
||||||
public PDFTableStripper() throws IOException
|
public PDFTableStripper() throws IOException {
|
||||||
{
|
|
||||||
super.setShouldSeparateByBeads(false);
|
super.setShouldSeparateByBeads(false);
|
||||||
regionStripper = new PDFTextStripperByArea();
|
regionStripper = new PDFTextStripperByArea();
|
||||||
regionStripper.setSortByPosition( true );
|
regionStripper.setSortByPosition(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -143,18 +131,15 @@ public class PDFTableStripper extends PDFTextStripper
|
|||||||
*
|
*
|
||||||
* @param rect The rectangle area to retrieve the text from.
|
* @param rect The rectangle area to retrieve the text from.
|
||||||
*/
|
*/
|
||||||
public void setRegion(Rectangle2D rect )
|
public void setRegion(Rectangle2D rect) {
|
||||||
{
|
|
||||||
regionArea = rect;
|
regionArea = rect;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getRows()
|
public int getRows() {
|
||||||
{
|
|
||||||
return nRows;
|
return nRows;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getColumns()
|
public int getColumns() {
|
||||||
{
|
|
||||||
return nCols;
|
return nCols;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,13 +148,11 @@ public class PDFTableStripper extends PDFTextStripper
|
|||||||
*
|
*
|
||||||
* @return The text that was identified in that region.
|
* @return The text that was identified in that region.
|
||||||
*/
|
*/
|
||||||
public String getText(int row, int col)
|
public String getText(int row, int col) {
|
||||||
{
|
return regionStripper.getTextForRegion("el" + col + "x" + row);
|
||||||
return regionStripper.getTextForRegion("el"+col+"x"+row);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void extractTable(PDPage pdPage) throws IOException
|
public void extractTable(PDPage pdPage) throws IOException {
|
||||||
{
|
|
||||||
setStartPage(getCurrentPageNo());
|
setStartPage(getCurrentPageNo());
|
||||||
setEndPage(getCurrentPageNo());
|
setEndPage(getCurrentPageNo());
|
||||||
|
|
||||||
@@ -182,11 +165,9 @@ public class PDFTableStripper extends PDFTextStripper
|
|||||||
// page may be rotated
|
// page may be rotated
|
||||||
rotateAT = new AffineTransform();
|
rotateAT = new AffineTransform();
|
||||||
int rotation = pdPage.getRotation();
|
int rotation = pdPage.getRotation();
|
||||||
if (rotation != 0)
|
if (rotation != 0) {
|
||||||
{
|
|
||||||
PDRectangle mediaBox = pdPage.getMediaBox();
|
PDRectangle mediaBox = pdPage.getMediaBox();
|
||||||
switch (rotation)
|
switch (rotation) {
|
||||||
{
|
|
||||||
case 90:
|
case 90:
|
||||||
rotateAT.translate(mediaBox.getHeight(), 0);
|
rotateAT.translate(mediaBox.getHeight(), 0);
|
||||||
break;
|
break;
|
||||||
@@ -209,11 +190,12 @@ public class PDFTableStripper extends PDFTextStripper
|
|||||||
|
|
||||||
Rectangle2D[][] regions = calculateTableRegions();
|
Rectangle2D[][] regions = calculateTableRegions();
|
||||||
|
|
||||||
// System.err.println("Drawing " + nCols + "x" + nRows + "="+ nRows*nCols + " regions");
|
// System.err.println("Drawing " + nCols + "x" + nRows + "="+ nRows*nCols + "
|
||||||
for(int i=0; i<nCols; ++i) {
|
// regions");
|
||||||
for(int j=0; j<nRows; ++j) {
|
for (int i = 0; i < nCols; ++i) {
|
||||||
|
for (int j = 0; j < nRows; ++j) {
|
||||||
final Rectangle2D region = regions[i][j];
|
final Rectangle2D region = regions[i][j];
|
||||||
regionStripper.addRegion("el"+i+"x"+j, region);
|
regionStripper.addRegion("el" + i + "x" + j, region);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,8 +205,8 @@ public class PDFTableStripper extends PDFTextStripper
|
|||||||
/**
|
/**
|
||||||
* Infer a rectangular grid of regions from the boxes field.
|
* Infer a rectangular grid of regions from the boxes field.
|
||||||
*
|
*
|
||||||
* @return 2D array of table regions (as Rectangle2D objects). Note that
|
* @return 2D array of table regions (as Rectangle2D objects). Note that some of these regions
|
||||||
* some of these regions may have no content.
|
* may have no content.
|
||||||
*/
|
*/
|
||||||
private Rectangle2D[][] calculateTableRegions() {
|
private Rectangle2D[][] calculateTableRegions() {
|
||||||
|
|
||||||
@@ -234,7 +216,7 @@ public class PDFTableStripper extends PDFTextStripper
|
|||||||
LinkedList<Interval> columns = new LinkedList<Interval>();
|
LinkedList<Interval> columns = new LinkedList<Interval>();
|
||||||
LinkedList<Interval> rows = new LinkedList<Interval>();
|
LinkedList<Interval> rows = new LinkedList<Interval>();
|
||||||
|
|
||||||
for(Rectangle2D box: boxes) {
|
for (Rectangle2D box : boxes) {
|
||||||
Interval x = new Interval(box.getMinX(), box.getMaxX());
|
Interval x = new Interval(box.getMinX(), box.getMaxX());
|
||||||
Interval y = new Interval(box.getMinY(), box.getMaxY());
|
Interval y = new Interval(box.getMinY(), box.getMaxY());
|
||||||
|
|
||||||
@@ -245,12 +227,17 @@ public class PDFTableStripper extends PDFTextStripper
|
|||||||
nRows = rows.size();
|
nRows = rows.size();
|
||||||
nCols = columns.size();
|
nCols = columns.size();
|
||||||
Rectangle2D[][] regions = new Rectangle2D[nCols][nRows];
|
Rectangle2D[][] regions = new Rectangle2D[nCols][nRows];
|
||||||
int i=0;
|
int i = 0;
|
||||||
// Label regions from top left, rather than the transformed orientation
|
// Label regions from top left, rather than the transformed orientation
|
||||||
for(Interval column: columns) {
|
for (Interval column : columns) {
|
||||||
int j=0;
|
int j = 0;
|
||||||
for(Interval row: rows) {
|
for (Interval row : rows) {
|
||||||
regions[nCols-i-1][nRows-j-1] = new Rectangle2D.Double(column.start, row.start, column.end - column.start, row.end - row.start);
|
regions[nCols - i - 1][nRows - j - 1] =
|
||||||
|
new Rectangle2D.Double(
|
||||||
|
column.start,
|
||||||
|
row.start,
|
||||||
|
column.end - column.start,
|
||||||
|
row.end - row.start);
|
||||||
++j;
|
++j;
|
||||||
}
|
}
|
||||||
++i;
|
++i;
|
||||||
@@ -260,18 +247,15 @@ public class PDFTableStripper extends PDFTextStripper
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register each character's bounding box, updating boxes field to maintain
|
* Register each character's bounding box, updating boxes field to maintain a list of all
|
||||||
* a list of all distinct groups of characters.
|
* distinct groups of characters.
|
||||||
*
|
*
|
||||||
* Overrides the default functionality of PDFTextStripper.
|
* <p>Overrides the default functionality of PDFTextStripper. Most of this is taken from
|
||||||
* Most of this is taken from DrawPrintTextLocations.java, with extra steps
|
* DrawPrintTextLocations.java, with extra steps at end of main loop
|
||||||
* at end of main loop
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void writeString(String string, List<TextPosition> textPositions) throws IOException
|
protected void writeString(String string, List<TextPosition> textPositions) throws IOException {
|
||||||
{
|
for (TextPosition text : textPositions) {
|
||||||
for (TextPosition text : textPositions)
|
|
||||||
{
|
|
||||||
// glyph space -> user space
|
// glyph space -> user space
|
||||||
// note: text.getTextMatrix() is *not* the Text Matrix, it's the Text Rendering Matrix
|
// note: text.getTextMatrix() is *not* the Text Matrix, it's the Text Rendering Matrix
|
||||||
AffineTransform at = text.getTextMatrix().createAffineTransform();
|
AffineTransform at = text.getTextMatrix().createAffineTransform();
|
||||||
@@ -279,37 +263,35 @@ public class PDFTableStripper extends PDFTextStripper
|
|||||||
BoundingBox bbox = font.getBoundingBox();
|
BoundingBox bbox = font.getBoundingBox();
|
||||||
|
|
||||||
// advance width, bbox height (glyph space)
|
// advance width, bbox height (glyph space)
|
||||||
float xadvance = font.getWidth(text.getCharacterCodes()[0]); // todo: should iterate all chars
|
float xadvance =
|
||||||
Rectangle2D.Float rect = new Rectangle2D.Float(0, bbox.getLowerLeftY(), xadvance, bbox.getHeight());
|
font.getWidth(text.getCharacterCodes()[0]); // todo: should iterate all chars
|
||||||
|
Rectangle2D.Float rect =
|
||||||
|
new Rectangle2D.Float(0, bbox.getLowerLeftY(), xadvance, bbox.getHeight());
|
||||||
|
|
||||||
if (font instanceof PDType3Font)
|
if (font instanceof PDType3Font) {
|
||||||
{
|
|
||||||
// bbox and font matrix are unscaled
|
// bbox and font matrix are unscaled
|
||||||
at.concatenate(font.getFontMatrix().createAffineTransform());
|
at.concatenate(font.getFontMatrix().createAffineTransform());
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
// bbox and font matrix are already scaled to 1000
|
// bbox and font matrix are already scaled to 1000
|
||||||
at.scale(1/1000f, 1/1000f);
|
at.scale(1 / 1000f, 1 / 1000f);
|
||||||
}
|
}
|
||||||
Shape s = at.createTransformedShape(rect);
|
Shape s = at.createTransformedShape(rect);
|
||||||
s = flipAT.createTransformedShape(s);
|
s = flipAT.createTransformedShape(s);
|
||||||
s = rotateAT.createTransformedShape(s);
|
s = rotateAT.createTransformedShape(s);
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Merge character's bounding box with boxes field
|
// Merge character's bounding box with boxes field
|
||||||
//
|
//
|
||||||
Rectangle2D bounds = s.getBounds2D();
|
Rectangle2D bounds = s.getBounds2D();
|
||||||
// Pad sides to detect almost touching boxes
|
// Pad sides to detect almost touching boxes
|
||||||
Rectangle2D hitbox = bounds.getBounds2D();
|
Rectangle2D hitbox = bounds.getBounds2D();
|
||||||
hitbox.add(bounds.getMinX() - dx , bounds.getMinY() - dy);
|
hitbox.add(bounds.getMinX() - dx, bounds.getMinY() - dy);
|
||||||
hitbox.add(bounds.getMaxX() + dx , bounds.getMaxY() + dy);
|
hitbox.add(bounds.getMaxX() + dx, bounds.getMaxY() + dy);
|
||||||
|
|
||||||
// Find all overlapping boxes
|
// Find all overlapping boxes
|
||||||
List<Rectangle2D> intersectList = new ArrayList<Rectangle2D>();
|
List<Rectangle2D> intersectList = new ArrayList<Rectangle2D>();
|
||||||
for(Rectangle2D box: boxes) {
|
for (Rectangle2D box : boxes) {
|
||||||
if(box.intersects(hitbox)) {
|
if (box.intersects(hitbox)) {
|
||||||
intersectList.add(box);
|
intersectList.add(box);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,38 +299,30 @@ public class PDFTableStripper extends PDFTextStripper
|
|||||||
// Combine all touching boxes and update
|
// Combine all touching boxes and update
|
||||||
// (NOTE: Potentially this could leave some overlapping boxes un-merged,
|
// (NOTE: Potentially this could leave some overlapping boxes un-merged,
|
||||||
// but it's sufficient for now and get's fixed up in calculateTableRegions)
|
// but it's sufficient for now and get's fixed up in calculateTableRegions)
|
||||||
for(Rectangle2D box: intersectList) {
|
for (Rectangle2D box : intersectList) {
|
||||||
bounds.add(box);
|
bounds.add(box);
|
||||||
boxes.remove(box);
|
boxes.remove(box);
|
||||||
}
|
}
|
||||||
boxes.add(bounds);
|
boxes.add(bounds);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method does nothing in this derived class, because beads and regions are incompatible. Beads are
|
* This method does nothing in this derived class, because beads and regions are incompatible.
|
||||||
* ignored when stripping by area.
|
* Beads are ignored when stripping by area.
|
||||||
*
|
*
|
||||||
* @param aShouldSeparateByBeads The new grouping of beads.
|
* @param aShouldSeparateByBeads The new grouping of beads.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public final void setShouldSeparateByBeads(boolean aShouldSeparateByBeads)
|
public final void setShouldSeparateByBeads(boolean aShouldSeparateByBeads) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/** Adapted from PDFTextStripperByArea {@inheritDoc} */
|
||||||
* Adapted from PDFTextStripperByArea
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
protected void processTextPosition( TextPosition text )
|
protected void processTextPosition(TextPosition text) {
|
||||||
{
|
if (regionArea != null && !regionArea.contains(text.getX(), text.getY())) {
|
||||||
if(regionArea!=null && !regionArea.contains( text.getX(), text.getY() ) ) {
|
|
||||||
// skip character
|
// skip character
|
||||||
} else {
|
} else {
|
||||||
super.processTextPosition( text );
|
super.processTextPosition(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
package stirling.software.SPDF.controller.web;
|
package stirling.software.SPDF.controller.web;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -14,122 +16,140 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import stirling.software.SPDF.model.Authority;
|
||||||
|
import stirling.software.SPDF.model.Role;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.repository.UserRepository;
|
import stirling.software.SPDF.repository.UserRepository;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@Tag(name = "Account Security", description = "Account Security APIs")
|
@Tag(name = "Account Security", description = "Account Security APIs")
|
||||||
public class AccountWebController {
|
public class AccountWebController {
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/login")
|
@GetMapping("/login")
|
||||||
public String login(HttpServletRequest request, Model model, Authentication authentication) {
|
public String login(HttpServletRequest request, Model model, Authentication authentication) {
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
return "redirect:/";
|
return "redirect:/";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.getParameter("error") != null) {
|
|
||||||
|
|
||||||
model.addAttribute("error", request.getParameter("error"));
|
if (request.getParameter("error") != null) {
|
||||||
}
|
|
||||||
if (request.getParameter("logout") != null) {
|
|
||||||
|
|
||||||
model.addAttribute("logoutMessage", "You have been logged out.");
|
model.addAttribute("error", request.getParameter("error"));
|
||||||
}
|
}
|
||||||
|
if (request.getParameter("logout") != null) {
|
||||||
return "login";
|
|
||||||
}
|
|
||||||
@Autowired
|
|
||||||
private UserRepository userRepository; // Assuming you have a repository for user operations
|
|
||||||
|
|
||||||
|
model.addAttribute("logoutMessage", "You have been logged out.");
|
||||||
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
return "login";
|
||||||
@GetMapping("/addUsers")
|
}
|
||||||
public String showAddUserForm(Model model, Authentication authentication) {
|
|
||||||
List<User> allUsers = userRepository.findAll();
|
|
||||||
model.addAttribute("users", allUsers);
|
|
||||||
model.addAttribute("currentUsername", authentication.getName());
|
|
||||||
return "addUsers";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserRepository userRepository; // Assuming you have a repository for user operations
|
||||||
@GetMapping("/account")
|
|
||||||
public String account(HttpServletRequest request, Model model, Authentication authentication) {
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
if (authentication == null || !authentication.isAuthenticated()) {
|
@GetMapping("/addUsers")
|
||||||
|
public String showAddUserForm(Model model, Authentication authentication) {
|
||||||
|
List<User> allUsers = userRepository.findAll();
|
||||||
|
Iterator<User> iterator = allUsers.iterator();
|
||||||
|
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
User user = iterator.next();
|
||||||
|
if (user != null) {
|
||||||
|
for (Authority authority : user.getAuthorities()) {
|
||||||
|
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
|
||||||
|
iterator.remove();
|
||||||
|
break; // Break out of the inner loop once the user is removed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
model.addAttribute("users", allUsers);
|
||||||
|
model.addAttribute("currentUsername", authentication.getName());
|
||||||
|
return "addUsers";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
|
@GetMapping("/account")
|
||||||
|
public String account(HttpServletRequest request, Model model, Authentication authentication) {
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
return "redirect:/";
|
return "redirect:/";
|
||||||
}
|
}
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
Object principal = authentication.getPrincipal();
|
Object principal = authentication.getPrincipal();
|
||||||
|
|
||||||
if (principal instanceof UserDetails) {
|
if (principal instanceof UserDetails) {
|
||||||
// Cast the principal object to UserDetails
|
// Cast the principal object to UserDetails
|
||||||
UserDetails userDetails = (UserDetails) principal;
|
UserDetails userDetails = (UserDetails) principal;
|
||||||
|
|
||||||
// Retrieve username and other attributes
|
// Retrieve username and other attributes
|
||||||
String username = userDetails.getUsername();
|
String username = userDetails.getUsername();
|
||||||
|
|
||||||
// Fetch user details from the database
|
// Fetch user details from the database
|
||||||
Optional<User> user = userRepository.findByUsername(username); // Assuming findByUsername method exists
|
Optional<User> user =
|
||||||
if (!user.isPresent()) {
|
userRepository.findByUsername(
|
||||||
// Handle error appropriately
|
username); // Assuming findByUsername method exists
|
||||||
return "redirect:/error"; // Example redirection in case of error
|
if (!user.isPresent()) {
|
||||||
}
|
// Handle error appropriately
|
||||||
|
return "redirect:/error"; // Example redirection in case of error
|
||||||
|
}
|
||||||
|
|
||||||
// Convert settings map to JSON string
|
// Convert settings map to JSON string
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
String settingsJson;
|
String settingsJson;
|
||||||
try {
|
try {
|
||||||
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
// Handle JSON conversion error
|
// Handle JSON conversion error
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return "redirect:/error"; // Example redirection in case of error
|
return "redirect:/error"; // Example redirection in case of error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add attributes to the model
|
// Add attributes to the model
|
||||||
model.addAttribute("username", username);
|
model.addAttribute("username", username);
|
||||||
model.addAttribute("role", user.get().getRolesAsString());
|
model.addAttribute("role", user.get().getRolesAsString());
|
||||||
model.addAttribute("settings", settingsJson);
|
model.addAttribute("settings", settingsJson);
|
||||||
model.addAttribute("changeCredsFlag", user.get().isFirstLogin());
|
model.addAttribute("changeCredsFlag", user.get().isFirstLogin());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return "redirect:/";
|
|
||||||
}
|
|
||||||
return "account";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/change-creds")
|
|
||||||
public String changeCreds(HttpServletRequest request, Model model, Authentication authentication) {
|
|
||||||
if (authentication == null || !authentication.isAuthenticated()) {
|
|
||||||
return "redirect:/";
|
return "redirect:/";
|
||||||
}
|
}
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
return "account";
|
||||||
Object principal = authentication.getPrincipal();
|
}
|
||||||
|
|
||||||
if (principal instanceof UserDetails) {
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
// Cast the principal object to UserDetails
|
@GetMapping("/change-creds")
|
||||||
UserDetails userDetails = (UserDetails) principal;
|
public String changeCreds(
|
||||||
|
HttpServletRequest request, Model model, Authentication authentication) {
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
return "redirect:/";
|
||||||
|
}
|
||||||
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
|
Object principal = authentication.getPrincipal();
|
||||||
|
|
||||||
// Retrieve username and other attributes
|
if (principal instanceof UserDetails) {
|
||||||
String username = userDetails.getUsername();
|
// Cast the principal object to UserDetails
|
||||||
|
UserDetails userDetails = (UserDetails) principal;
|
||||||
|
|
||||||
// Fetch user details from the database
|
// Retrieve username and other attributes
|
||||||
Optional<User> user = userRepository.findByUsername(username); // Assuming findByUsername method exists
|
String username = userDetails.getUsername();
|
||||||
if (!user.isPresent()) {
|
|
||||||
// Handle error appropriately
|
// Fetch user details from the database
|
||||||
return "redirect:/error"; // Example redirection in case of error
|
Optional<User> user =
|
||||||
}
|
userRepository.findByUsername(
|
||||||
// Add attributes to the model
|
username); // Assuming findByUsername method exists
|
||||||
model.addAttribute("username", username);
|
if (!user.isPresent()) {
|
||||||
}
|
// Handle error appropriately
|
||||||
} else {
|
return "redirect:/error"; // Example redirection in case of error
|
||||||
return "redirect:/";
|
}
|
||||||
}
|
// Add attributes to the model
|
||||||
return "change-creds";
|
model.addAttribute("username", username);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return "redirect:/";
|
||||||
|
}
|
||||||
|
return "change-creds";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ public class ConverterWebController {
|
|||||||
model.addAttribute("currentPage", "html-to-pdf");
|
model.addAttribute("currentPage", "html-to-pdf");
|
||||||
return "convert/html-to-pdf";
|
return "convert/html-to-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/markdown-to-pdf")
|
@GetMapping("/markdown-to-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String convertMarkdownToPdfForm(Model model) {
|
public String convertMarkdownToPdfForm(Model model) {
|
||||||
model.addAttribute("currentPage", "markdown-to-pdf");
|
model.addAttribute("currentPage", "markdown-to-pdf");
|
||||||
return "convert/markdown-to-pdf";
|
return "convert/markdown-to-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/url-to-pdf")
|
@GetMapping("/url-to-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String convertURLToPdfForm(Model model) {
|
public String convertURLToPdfForm(Model model) {
|
||||||
@@ -40,25 +40,22 @@ public class ConverterWebController {
|
|||||||
return "convert/url-to-pdf";
|
return "convert/url-to-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/pdf-to-img")
|
@GetMapping("/pdf-to-img")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String pdfToimgForm(Model model) {
|
public String pdfToimgForm(Model model) {
|
||||||
model.addAttribute("currentPage", "pdf-to-img");
|
model.addAttribute("currentPage", "pdf-to-img");
|
||||||
return "convert/pdf-to-img";
|
return "convert/pdf-to-img";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/file-to-pdf")
|
@GetMapping("/file-to-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String convertToPdfForm(Model model) {
|
public String convertToPdfForm(Model model) {
|
||||||
model.addAttribute("currentPage", "file-to-pdf");
|
model.addAttribute("currentPage", "file-to-pdf");
|
||||||
return "convert/file-to-pdf";
|
return "convert/file-to-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// PDF TO......
|
||||||
//PDF TO......
|
|
||||||
|
|
||||||
@GetMapping("/pdf-to-html")
|
@GetMapping("/pdf-to-html")
|
||||||
@Hidden
|
@Hidden
|
||||||
public ModelAndView pdfToHTML() {
|
public ModelAndView pdfToHTML() {
|
||||||
@@ -107,7 +104,6 @@ public class ConverterWebController {
|
|||||||
return modelAndView;
|
return modelAndView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/pdf-to-pdfa")
|
@GetMapping("/pdf-to-pdfa")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String pdfToPdfAForm(Model model) {
|
public String pdfToPdfAForm(Model model) {
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
package stirling.software.SPDF.controller.web;
|
package stirling.software.SPDF.controller.web;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import java.io.File;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import java.io.IOException;
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
import java.nio.charset.StandardCharsets;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.core.io.ResourceLoader;
|
import org.springframework.core.io.ResourceLoader;
|
||||||
@@ -12,62 +23,65 @@ import org.springframework.stereotype.Controller;
|
|||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
import java.io.IOException;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import java.nio.charset.StandardCharsets;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
import java.nio.file.Paths;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.util.*;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@Tag(name = "General", description = "General APIs")
|
@Tag(name = "General", description = "General APIs")
|
||||||
public class GeneralWebController {
|
public class GeneralWebController {
|
||||||
|
|
||||||
|
|
||||||
|
@GetMapping("/pipeline")
|
||||||
|
@Hidden
|
||||||
@GetMapping("/pipeline")
|
public String pipelineForm(Model model) {
|
||||||
@Hidden
|
model.addAttribute("currentPage", "pipeline");
|
||||||
public String pipelineForm(Model model) {
|
|
||||||
model.addAttribute("currentPage", "pipeline");
|
|
||||||
|
|
||||||
List<String> pipelineConfigs = new ArrayList<>();
|
List<String> pipelineConfigs = new ArrayList<>();
|
||||||
try (Stream<Path> paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) {
|
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
|
||||||
List<Path> jsonFiles = paths
|
|
||||||
.filter(Files::isRegularFile)
|
|
||||||
.filter(p -> p.toString().endsWith(".json"))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
for (Path jsonFile : jsonFiles) {
|
if (new File("./pipeline/defaultWebUIConfigs/").exists()) {
|
||||||
String content = Files.readString(jsonFile, StandardCharsets.UTF_8);
|
try (Stream<Path> paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) {
|
||||||
pipelineConfigs.add(content);
|
List<Path> jsonFiles =
|
||||||
}
|
paths.filter(Files::isRegularFile)
|
||||||
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
|
.filter(p -> p.toString().endsWith(".json"))
|
||||||
for (String config : pipelineConfigs) {
|
.collect(Collectors.toList());
|
||||||
Map<String, Object> jsonContent = new ObjectMapper().readValue(config, new TypeReference<Map<String, Object>>(){});
|
|
||||||
|
|
||||||
String name = (String) jsonContent.get("name");
|
for (Path jsonFile : jsonFiles) {
|
||||||
Map<String, String> configWithName = new HashMap<>();
|
String content = Files.readString(jsonFile, StandardCharsets.UTF_8);
|
||||||
configWithName.put("json", config);
|
pipelineConfigs.add(content);
|
||||||
configWithName.put("name", name);
|
}
|
||||||
pipelineConfigsWithNames.add(configWithName);
|
|
||||||
}
|
|
||||||
model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames);
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
for (String config : pipelineConfigs) {
|
||||||
e.printStackTrace();
|
Map<String, Object> jsonContent =
|
||||||
}
|
new ObjectMapper()
|
||||||
|
.readValue(config, new TypeReference<Map<String, Object>>() {});
|
||||||
|
|
||||||
model.addAttribute("pipelineConfigs", pipelineConfigs);
|
String name = (String) jsonContent.get("name");
|
||||||
|
Map<String, String> configWithName = new HashMap<>();
|
||||||
|
configWithName.put("json", config);
|
||||||
|
configWithName.put("name", name);
|
||||||
|
pipelineConfigsWithNames.add(configWithName);
|
||||||
|
}
|
||||||
|
|
||||||
return "pipeline";
|
} catch (IOException e) {
|
||||||
}
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pipelineConfigsWithNames.size() == 0) {
|
||||||
|
Map<String, String> configWithName = new HashMap<>();
|
||||||
|
configWithName.put("json", "");
|
||||||
|
configWithName.put("name", "No preloaded configs found");
|
||||||
|
pipelineConfigsWithNames.add(configWithName);
|
||||||
|
}
|
||||||
|
model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames);
|
||||||
|
|
||||||
|
model.addAttribute("pipelineConfigs", pipelineConfigs);
|
||||||
|
|
||||||
|
return "pipeline";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/merge-pdfs")
|
@GetMapping("/merge-pdfs")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String mergePdfForm(Model model) {
|
public String mergePdfForm(Model model) {
|
||||||
@@ -75,63 +89,69 @@ public class GeneralWebController {
|
|||||||
return "merge-pdfs";
|
return "merge-pdfs";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/split-pdf-by-sections")
|
||||||
|
@Hidden
|
||||||
|
public String splitPdfBySections(Model model) {
|
||||||
|
model.addAttribute("currentPage", "split-pdf-by-sections");
|
||||||
|
return "split-pdf-by-sections";
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/view-pdf")
|
@GetMapping("/view-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String ViewPdfForm2(Model model) {
|
public String ViewPdfForm2(Model model) {
|
||||||
model.addAttribute("currentPage", "view-pdf");
|
model.addAttribute("currentPage", "view-pdf");
|
||||||
return "view-pdf";
|
return "view-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/multi-tool")
|
@GetMapping("/multi-tool")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String multiToolForm(Model model) {
|
public String multiToolForm(Model model) {
|
||||||
model.addAttribute("currentPage", "multi-tool");
|
model.addAttribute("currentPage", "multi-tool");
|
||||||
return "multi-tool";
|
return "multi-tool";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/remove-pages")
|
@GetMapping("/remove-pages")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String pageDeleter(Model model) {
|
public String pageDeleter(Model model) {
|
||||||
model.addAttribute("currentPage", "remove-pages");
|
model.addAttribute("currentPage", "remove-pages");
|
||||||
return "remove-pages";
|
return "remove-pages";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/pdf-organizer")
|
@GetMapping("/pdf-organizer")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String pageOrganizer(Model model) {
|
public String pageOrganizer(Model model) {
|
||||||
model.addAttribute("currentPage", "pdf-organizer");
|
model.addAttribute("currentPage", "pdf-organizer");
|
||||||
return "pdf-organizer";
|
return "pdf-organizer";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/extract-page")
|
@GetMapping("/extract-page")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String extractPages(Model model) {
|
public String extractPages(Model model) {
|
||||||
model.addAttribute("currentPage", "extract-page");
|
model.addAttribute("currentPage", "extract-page");
|
||||||
return "extract-page";
|
return "extract-page";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/pdf-to-single-page")
|
@GetMapping("/pdf-to-single-page")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String pdfToSinglePage(Model model) {
|
public String pdfToSinglePage(Model model) {
|
||||||
model.addAttribute("currentPage", "pdf-to-single-page");
|
model.addAttribute("currentPage", "pdf-to-single-page");
|
||||||
return "pdf-to-single-page";
|
return "pdf-to-single-page";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/rotate-pdf")
|
@GetMapping("/rotate-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String rotatePdfForm(Model model) {
|
public String rotatePdfForm(Model model) {
|
||||||
model.addAttribute("currentPage", "rotate-pdf");
|
model.addAttribute("currentPage", "rotate-pdf");
|
||||||
return "rotate-pdf";
|
return "rotate-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/split-pdfs")
|
@GetMapping("/split-pdfs")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String splitPdfForm(Model model) {
|
public String splitPdfForm(Model model) {
|
||||||
model.addAttribute("currentPage", "split-pdfs");
|
model.addAttribute("currentPage", "split-pdfs");
|
||||||
return "split-pdfs";
|
return "split-pdfs";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/sign")
|
@GetMapping("/sign")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String signForm(Model model) {
|
public String signForm(Model model) {
|
||||||
@@ -139,27 +159,45 @@ public class GeneralWebController {
|
|||||||
model.addAttribute("fonts", getFontNames());
|
model.addAttribute("fonts", getFontNames());
|
||||||
return "sign";
|
return "sign";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/add-elements")
|
||||||
|
@Hidden
|
||||||
|
public String addElements(Model model) {
|
||||||
|
model.addAttribute("currentPage", "add-elements");
|
||||||
|
model.addAttribute("fonts", getFontNames());
|
||||||
|
return "add-elements";
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/multi-page-layout")
|
@GetMapping("/multi-page-layout")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String multiPageLayoutForm(Model model) {
|
public String multiPageLayoutForm(Model model) {
|
||||||
model.addAttribute("currentPage", "multi-page-layout");
|
model.addAttribute("currentPage", "multi-page-layout");
|
||||||
return "multi-page-layout";
|
return "multi-page-layout";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/scale-pages")
|
@GetMapping("/scale-pages")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String scalePagesFrom(Model model) {
|
public String scalePagesFrom(Model model) {
|
||||||
model.addAttribute("currentPage", "scale-pages");
|
model.addAttribute("currentPage", "scale-pages");
|
||||||
return "scale-pages";
|
return "scale-pages";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/split-by-size-or-count")
|
||||||
|
@Hidden
|
||||||
@Autowired
|
public String splitBySizeOrCount(Model model) {
|
||||||
private ResourceLoader resourceLoader;
|
model.addAttribute("currentPage", "split-by-size-or-count");
|
||||||
|
return "split-by-size-or-count";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/overlay-pdf")
|
||||||
|
@Hidden
|
||||||
|
public String overlayPdf(Model model) {
|
||||||
|
model.addAttribute("currentPage", "overlay-pdf");
|
||||||
|
return "overlay-pdf";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired private ResourceLoader resourceLoader;
|
||||||
|
|
||||||
private List<FontResource> getFontNames() {
|
private List<FontResource> getFontNames() {
|
||||||
List<FontResource> fontNames = new ArrayList<>();
|
List<FontResource> fontNames = new ArrayList<>();
|
||||||
|
|
||||||
@@ -174,25 +212,27 @@ public class GeneralWebController {
|
|||||||
|
|
||||||
private List<FontResource> getFontNamesFromLocation(String locationPattern) {
|
private List<FontResource> getFontNamesFromLocation(String locationPattern) {
|
||||||
try {
|
try {
|
||||||
Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
|
Resource[] resources =
|
||||||
.getResources(locationPattern);
|
ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
|
||||||
|
.getResources(locationPattern);
|
||||||
return Arrays.stream(resources)
|
return Arrays.stream(resources)
|
||||||
.map(resource -> {
|
.map(
|
||||||
try {
|
resource -> {
|
||||||
String filename = resource.getFilename();
|
try {
|
||||||
if (filename != null) {
|
String filename = resource.getFilename();
|
||||||
int lastDotIndex = filename.lastIndexOf('.');
|
if (filename != null) {
|
||||||
if (lastDotIndex != -1) {
|
int lastDotIndex = filename.lastIndexOf('.');
|
||||||
String name = filename.substring(0, lastDotIndex);
|
if (lastDotIndex != -1) {
|
||||||
String extension = filename.substring(lastDotIndex + 1);
|
String name = filename.substring(0, lastDotIndex);
|
||||||
return new FontResource(name, extension);
|
String extension = filename.substring(lastDotIndex + 1);
|
||||||
|
return new FontResource(name, extension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Error processing filename", e);
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
return null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException("Error processing filename", e);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -200,64 +240,65 @@ public class GeneralWebController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public String getFormatFromExtension(String extension) {
|
public String getFormatFromExtension(String extension) {
|
||||||
switch (extension) {
|
switch (extension) {
|
||||||
case "ttf": return "truetype";
|
case "ttf":
|
||||||
case "woff": return "woff";
|
return "truetype";
|
||||||
case "woff2": return "woff2";
|
case "woff":
|
||||||
case "eot": return "embedded-opentype";
|
return "woff";
|
||||||
case "svg": return "svg";
|
case "woff2":
|
||||||
default: return ""; // or throw an exception if an unexpected extension is encountered
|
return "woff2";
|
||||||
|
case "eot":
|
||||||
|
return "embedded-opentype";
|
||||||
|
case "svg":
|
||||||
|
return "svg";
|
||||||
|
default:
|
||||||
|
return ""; // or throw an exception if an unexpected extension is encountered
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public class FontResource {
|
public class FontResource {
|
||||||
private String name;
|
private String name;
|
||||||
private String extension;
|
private String extension;
|
||||||
private String type;
|
private String type;
|
||||||
|
|
||||||
public FontResource(String name, String extension) {
|
public FontResource(String name, String extension) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.extension = extension;
|
this.extension = extension;
|
||||||
this.type = getFormatFromExtension(extension);
|
this.type = getFormatFromExtension(extension);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setName(String name) {
|
public void setName(String name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getExtension() {
|
public String getExtension() {
|
||||||
return extension;
|
return extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setExtension(String extension) {
|
public void setExtension(String extension) {
|
||||||
this.extension = extension;
|
this.extension = extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getType() {
|
public String getType() {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setType(String type) {
|
public void setType(String type) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/crop")
|
@GetMapping("/crop")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String cropForm(Model model) {
|
public String cropForm(Model model) {
|
||||||
model.addAttribute("currentPage", "crop");
|
model.addAttribute("currentPage", "crop");
|
||||||
return "crop";
|
return "crop";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/auto-split-pdf")
|
@GetMapping("/auto-split-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
|
|||||||
@@ -8,20 +8,19 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
public class HomeWebController {
|
public class HomeWebController {
|
||||||
|
|
||||||
@GetMapping("/about")
|
@GetMapping("/about")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String gameForm(Model model) {
|
public String gameForm(Model model) {
|
||||||
model.addAttribute("currentPage", "about");
|
model.addAttribute("currentPage", "about");
|
||||||
return "about";
|
return "about";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/")
|
@GetMapping("/")
|
||||||
public String home(Model model) {
|
public String home(Model model) {
|
||||||
model.addAttribute("currentPage", "home");
|
model.addAttribute("currentPage", "home");
|
||||||
@@ -32,21 +31,18 @@ public class HomeWebController {
|
|||||||
public String root(Model model) {
|
public String root(Model model) {
|
||||||
return "redirect:/";
|
return "redirect:/";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Autowired
|
|
||||||
ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
|
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
@Hidden
|
@Hidden
|
||||||
public String getRobotsTxt() {
|
public String getRobotsTxt() {
|
||||||
Boolean allowGoogle = applicationProperties.getSystem().getGooglevisibility();
|
Boolean allowGoogle = applicationProperties.getSystem().getGooglevisibility();
|
||||||
if(Boolean.TRUE.equals(allowGoogle)) {
|
if (Boolean.TRUE.equals(allowGoogle)) {
|
||||||
return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /";
|
return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /";
|
||||||
} else {
|
} else {
|
||||||
return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /";
|
return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
package stirling.software.SPDF.controller.web;
|
package stirling.software.SPDF.controller.web;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@@ -22,39 +23,38 @@ import io.micrometer.core.instrument.MeterRegistry;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import stirling.software.SPDF.config.StartupApplicationListener;
|
import stirling.software.SPDF.config.StartupApplicationListener;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1")
|
@RequestMapping("/api/v1/info")
|
||||||
@Tag(name = "API", description = "Info APIs")
|
@Tag(name = "Info", description = "Info APIs")
|
||||||
public class MetricsController {
|
public class MetricsController {
|
||||||
|
|
||||||
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
@Autowired
|
|
||||||
ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
|
|
||||||
private final MeterRegistry meterRegistry;
|
private final MeterRegistry meterRegistry;
|
||||||
|
|
||||||
private boolean metricsEnabled;
|
private boolean metricsEnabled;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
Boolean metricsEnabled = applicationProperties.getMetrics().getEnabled();
|
Boolean metricsEnabled = applicationProperties.getMetrics().getEnabled();
|
||||||
if(metricsEnabled == null)
|
if (metricsEnabled == null) metricsEnabled = true;
|
||||||
metricsEnabled = true;
|
|
||||||
this.metricsEnabled = metricsEnabled;
|
this.metricsEnabled = metricsEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MetricsController(MeterRegistry meterRegistry) {
|
public MetricsController(MeterRegistry meterRegistry) {
|
||||||
this.meterRegistry = meterRegistry;
|
this.meterRegistry = meterRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/status")
|
@GetMapping("/status")
|
||||||
@Operation(summary = "Application status and version",
|
@Operation(
|
||||||
description = "This endpoint returns the status of the application and its version number.")
|
summary = "Application status and version",
|
||||||
|
description =
|
||||||
|
"This endpoint returns the status of the application and its version number.")
|
||||||
public ResponseEntity<?> getStatus() {
|
public ResponseEntity<?> getStatus() {
|
||||||
if (!metricsEnabled) {
|
if (!metricsEnabled) {
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
|
||||||
@@ -65,38 +65,46 @@ public class MetricsController {
|
|||||||
status.put("version", getClass().getPackage().getImplementationVersion());
|
status.put("version", getClass().getPackage().getImplementationVersion());
|
||||||
return ResponseEntity.ok(status);
|
return ResponseEntity.ok(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/loads")
|
@GetMapping("/loads")
|
||||||
@Operation(summary = "GET request count",
|
@Operation(
|
||||||
description = "This endpoint returns the total count of GET requests or the count of GET requests for a specific endpoint.")
|
summary = "GET request count",
|
||||||
public ResponseEntity<?> getPageLoads(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional<String> endpoint) {
|
description =
|
||||||
if (!metricsEnabled) {
|
"This endpoint returns the total count of GET requests or the count of GET requests for a specific endpoint.")
|
||||||
|
public ResponseEntity<?> getPageLoads(
|
||||||
|
@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint")
|
||||||
|
Optional<String> endpoint) {
|
||||||
|
if (!metricsEnabled) {
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
||||||
double count = 0.0;
|
double count = 0.0;
|
||||||
|
|
||||||
for (Meter meter : meterRegistry.getMeters()) {
|
for (Meter meter : meterRegistry.getMeters()) {
|
||||||
if (meter.getId().getName().equals("http.requests")) {
|
if (meter.getId().getName().equals("http.requests")) {
|
||||||
String method = meter.getId().getTag("method");
|
String method = meter.getId().getTag("method");
|
||||||
if (method != null && method.equals("GET")) {
|
if (method != null && method.equals("GET")) {
|
||||||
|
|
||||||
if (endpoint.isPresent() && !endpoint.get().isBlank()) {
|
if (endpoint.isPresent() && !endpoint.get().isBlank()) {
|
||||||
if(!endpoint.get().startsWith("/")) {
|
if (!endpoint.get().startsWith("/")) {
|
||||||
endpoint = Optional.of("/" + endpoint.get());
|
endpoint = Optional.of("/" + endpoint.get());
|
||||||
}
|
}
|
||||||
System.out.println("loads " + endpoint.get() + " vs " + meter.getId().getTag("uri"));
|
System.out.println(
|
||||||
if(endpoint.get().equals(meter.getId().getTag("uri"))){
|
"loads "
|
||||||
if (meter instanceof Counter) {
|
+ endpoint.get()
|
||||||
count += ((Counter) meter).count();
|
+ " vs "
|
||||||
}
|
+ meter.getId().getTag("uri"));
|
||||||
}
|
if (endpoint.get().equals(meter.getId().getTag("uri"))) {
|
||||||
} else {
|
if (meter instanceof Counter) {
|
||||||
if (meter instanceof Counter) {
|
count += ((Counter) meter).count();
|
||||||
count += ((Counter) meter).count();
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
if (meter instanceof Counter) {
|
||||||
|
count += ((Counter) meter).count();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,10 +116,11 @@ public class MetricsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/loads/all")
|
@GetMapping("/loads/all")
|
||||||
@Operation(summary = "GET requests count for all endpoints",
|
@Operation(
|
||||||
|
summary = "GET requests count for all endpoints",
|
||||||
description = "This endpoint returns the count of GET requests for each endpoint.")
|
description = "This endpoint returns the count of GET requests for each endpoint.")
|
||||||
public ResponseEntity<?> getAllEndpointLoads() {
|
public ResponseEntity<?> getAllEndpointLoads() {
|
||||||
if (!metricsEnabled) {
|
if (!metricsEnabled) {
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -133,10 +142,11 @@ public class MetricsController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<EndpointCount> results = counts.entrySet().stream()
|
List<EndpointCount> results =
|
||||||
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
|
counts.entrySet().stream()
|
||||||
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
|
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
|
||||||
.collect(Collectors.toList());
|
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
return ResponseEntity.ok(results);
|
return ResponseEntity.ok(results);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -147,40 +157,47 @@ public class MetricsController {
|
|||||||
public class EndpointCount {
|
public class EndpointCount {
|
||||||
private String endpoint;
|
private String endpoint;
|
||||||
private double count;
|
private double count;
|
||||||
|
|
||||||
public EndpointCount(String endpoint, double count) {
|
|
||||||
this.endpoint = endpoint;
|
|
||||||
this.count = count;
|
|
||||||
}
|
|
||||||
public String getEndpoint() {
|
|
||||||
return endpoint;
|
|
||||||
}
|
|
||||||
public void setEndpoint(String endpoint) {
|
|
||||||
this.endpoint = endpoint;
|
|
||||||
}
|
|
||||||
public double getCount() {
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
public void setCount(double count) {
|
|
||||||
this.count = count;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public EndpointCount(String endpoint, double count) {
|
||||||
|
this.endpoint = endpoint;
|
||||||
|
this.count = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEndpoint() {
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEndpoint(String endpoint) {
|
||||||
|
this.endpoint = endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getCount() {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCount(double count) {
|
||||||
|
this.count = count;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/requests")
|
@GetMapping("/requests")
|
||||||
@Operation(summary = "POST request count",
|
@Operation(
|
||||||
description = "This endpoint returns the total count of POST requests or the count of POST requests for a specific endpoint.")
|
summary = "POST request count",
|
||||||
public ResponseEntity<?> getTotalRequests(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional<String> endpoint) {
|
description =
|
||||||
if (!metricsEnabled) {
|
"This endpoint returns the total count of POST requests or the count of POST requests for a specific endpoint.")
|
||||||
|
public ResponseEntity<?> getTotalRequests(
|
||||||
|
@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint")
|
||||||
|
Optional<String> endpoint) {
|
||||||
|
if (!metricsEnabled) {
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
double count = 0.0;
|
double count = 0.0;
|
||||||
|
|
||||||
for (Meter meter : meterRegistry.getMeters()) {
|
for (Meter meter : meterRegistry.getMeters()) {
|
||||||
if (meter.getId().getName().equals("http.requests")) {
|
if (meter.getId().getName().equals("http.requests")) {
|
||||||
String method = meter.getId().getTag("method");
|
String method = meter.getId().getTag("method");
|
||||||
|
System.out.println("method=" + method + ", endpont=" + endpoint.get());
|
||||||
if (method != null && method.equals("POST")) {
|
if (method != null && method.equals("POST")) {
|
||||||
if (endpoint.isPresent() && !endpoint.get().isBlank()) {
|
if (endpoint.isPresent() && !endpoint.get().isBlank()) {
|
||||||
if (!endpoint.get().startsWith("/")) {
|
if (!endpoint.get().startsWith("/")) {
|
||||||
@@ -199,28 +216,33 @@ public class MetricsController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ResponseEntity.ok(count);
|
return ResponseEntity.ok(count);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return ResponseEntity.ok(-1);
|
return ResponseEntity.ok(-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/requests/all")
|
@GetMapping("/requests/all")
|
||||||
@Operation(summary = "POST requests count for all endpoints",
|
@Operation(
|
||||||
|
summary = "POST requests count for all endpoints",
|
||||||
description = "This endpoint returns the count of POST requests for each endpoint.")
|
description = "This endpoint returns the count of POST requests for each endpoint.")
|
||||||
public ResponseEntity<?> getAllPostRequests() {
|
public ResponseEntity<?> getAllPostRequests() {
|
||||||
if (!metricsEnabled) {
|
if (!metricsEnabled) {
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Map<String, Double> counts = new HashMap<>();
|
Map<String, Double> counts = new HashMap<>();
|
||||||
|
|
||||||
for (Meter meter : meterRegistry.getMeters()) {
|
for (Meter meter : meterRegistry.getMeters()) {
|
||||||
|
|
||||||
|
System.out.println("meter.getId().getName()=" + meter.getId().getName());
|
||||||
if (meter.getId().getName().equals("http.requests")) {
|
if (meter.getId().getName().equals("http.requests")) {
|
||||||
String method = meter.getId().getTag("method");
|
String method = meter.getId().getTag("method");
|
||||||
|
System.out.println("method=" + method );
|
||||||
if (method != null && method.equals("POST")) {
|
if (method != null && method.equals("POST")) {
|
||||||
String uri = meter.getId().getTag("uri");
|
String uri = meter.getId().getTag("uri");
|
||||||
|
System.out.println("method=" + method + ", endpont=" + meter.getId());
|
||||||
|
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
double currentCount = counts.getOrDefault(uri, 0.0);
|
double currentCount = counts.getOrDefault(uri, 0.0);
|
||||||
if (meter instanceof Counter) {
|
if (meter instanceof Counter) {
|
||||||
@@ -232,10 +254,11 @@ public class MetricsController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<EndpointCount> results = counts.entrySet().stream()
|
List<EndpointCount> results =
|
||||||
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
|
counts.entrySet().stream()
|
||||||
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
|
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
|
||||||
.collect(Collectors.toList());
|
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
return ResponseEntity.ok(results);
|
return ResponseEntity.ok(results);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -243,7 +266,6 @@ public class MetricsController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/uptime")
|
@GetMapping("/uptime")
|
||||||
public ResponseEntity<?> getUptime() {
|
public ResponseEntity<?> getUptime() {
|
||||||
if (!metricsEnabled) {
|
if (!metricsEnabled) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class OtherWebController {
|
|||||||
model.addAttribute("currentPage", "compress-pdf");
|
model.addAttribute("currentPage", "compress-pdf");
|
||||||
return "misc/compress-pdf";
|
return "misc/compress-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/extract-image-scans")
|
@GetMapping("/extract-image-scans")
|
||||||
@Hidden
|
@Hidden
|
||||||
public ModelAndView extractImageScansForm() {
|
public ModelAndView extractImageScansForm() {
|
||||||
@@ -31,37 +31,34 @@ public class OtherWebController {
|
|||||||
modelAndView.addObject("currentPage", "extract-image-scans");
|
modelAndView.addObject("currentPage", "extract-image-scans");
|
||||||
return modelAndView;
|
return modelAndView;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/show-javascript")
|
@GetMapping("/show-javascript")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String extractJavascriptForm(Model model) {
|
public String extractJavascriptForm(Model model) {
|
||||||
model.addAttribute("currentPage", "show-javascript");
|
model.addAttribute("currentPage", "show-javascript");
|
||||||
return "misc/show-javascript";
|
return "misc/show-javascript";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/add-page-numbers")
|
@GetMapping("/add-page-numbers")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String addPageNumbersForm(Model model) {
|
public String addPageNumbersForm(Model model) {
|
||||||
model.addAttribute("currentPage", "add-page-numbers");
|
model.addAttribute("currentPage", "add-page-numbers");
|
||||||
return "misc/add-page-numbers";
|
return "misc/add-page-numbers";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/extract-images")
|
@GetMapping("/extract-images")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String extractImagesForm(Model model) {
|
public String extractImagesForm(Model model) {
|
||||||
model.addAttribute("currentPage", "extract-images");
|
model.addAttribute("currentPage", "extract-images");
|
||||||
return "misc/extract-images";
|
return "misc/extract-images";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/flatten")
|
@GetMapping("/flatten")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String flattenForm(Model model) {
|
public String flattenForm(Model model) {
|
||||||
model.addAttribute("currentPage", "flatten");
|
model.addAttribute("currentPage", "flatten");
|
||||||
return "misc/flatten";
|
return "misc/flatten";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/change-metadata")
|
@GetMapping("/change-metadata")
|
||||||
@Hidden
|
@Hidden
|
||||||
@@ -69,22 +66,25 @@ public class OtherWebController {
|
|||||||
model.addAttribute("currentPage", "change-metadata");
|
model.addAttribute("currentPage", "change-metadata");
|
||||||
return "misc/change-metadata";
|
return "misc/change-metadata";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/compare")
|
@GetMapping("/compare")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String compareForm(Model model) {
|
public String compareForm(Model model) {
|
||||||
model.addAttribute("currentPage", "compare");
|
model.addAttribute("currentPage", "compare");
|
||||||
return "misc/compare";
|
return "misc/compare";
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getAvailableTesseractLanguages() {
|
public List<String> getAvailableTesseractLanguages() {
|
||||||
String tessdataDir = "/usr/share/tesseract-ocr/5/tessdata";
|
String tessdataDir = "/usr/share/tesseract-ocr/5/tessdata";
|
||||||
File[] files = new File(tessdataDir).listFiles();
|
File[] files = new File(tessdataDir).listFiles();
|
||||||
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/ocr-pdf")
|
@GetMapping("/ocr-pdf")
|
||||||
@@ -97,7 +97,6 @@ public class OtherWebController {
|
|||||||
modelAndView.addObject("currentPage", "ocr-pdf");
|
modelAndView.addObject("currentPage", "ocr-pdf");
|
||||||
return modelAndView;
|
return modelAndView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/add-image")
|
@GetMapping("/add-image")
|
||||||
@Hidden
|
@Hidden
|
||||||
@@ -105,28 +104,34 @@ public class OtherWebController {
|
|||||||
model.addAttribute("currentPage", "add-image");
|
model.addAttribute("currentPage", "add-image");
|
||||||
return "misc/add-image";
|
return "misc/add-image";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/adjust-contrast")
|
@GetMapping("/adjust-contrast")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String contrast(Model model) {
|
public String contrast(Model model) {
|
||||||
model.addAttribute("currentPage", "adjust-contrast");
|
model.addAttribute("currentPage", "adjust-contrast");
|
||||||
return "misc/adjust-contrast";
|
return "misc/adjust-contrast";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/repair")
|
@GetMapping("/repair")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String repairForm(Model model) {
|
public String repairForm(Model model) {
|
||||||
model.addAttribute("currentPage", "repair");
|
model.addAttribute("currentPage", "repair");
|
||||||
return "misc/repair";
|
return "misc/repair";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/remove-blanks")
|
@GetMapping("/remove-blanks")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String removeBlanksForm(Model model) {
|
public String removeBlanksForm(Model model) {
|
||||||
model.addAttribute("currentPage", "remove-blanks");
|
model.addAttribute("currentPage", "remove-blanks");
|
||||||
return "misc/remove-blanks";
|
return "misc/remove-blanks";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/remove-annotations")
|
||||||
|
@Hidden
|
||||||
|
public String removeAnnotationsForm(Model model) {
|
||||||
|
model.addAttribute("currentPage", "remove-annotations");
|
||||||
|
return "misc/remove-annotations";
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/auto-crop")
|
@GetMapping("/auto-crop")
|
||||||
@Hidden
|
@Hidden
|
||||||
@@ -134,14 +139,11 @@ public class OtherWebController {
|
|||||||
model.addAttribute("currentPage", "auto-crop");
|
model.addAttribute("currentPage", "auto-crop");
|
||||||
return "misc/auto-crop";
|
return "misc/auto-crop";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/auto-rename")
|
@GetMapping("/auto-rename")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String autoRenameForm(Model model) {
|
public String autoRenameForm(Model model) {
|
||||||
model.addAttribute("currentPage", "auto-rename");
|
model.addAttribute("currentPage", "auto-rename");
|
||||||
return "misc/auto-rename";
|
return "misc/auto-rename";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,20 +10,21 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
@Controller
|
@Controller
|
||||||
@Tag(name = "Security", description = "Security APIs")
|
@Tag(name = "Security", description = "Security APIs")
|
||||||
public class SecurityWebController {
|
public class SecurityWebController {
|
||||||
|
|
||||||
@GetMapping("/auto-redact")
|
@GetMapping("/auto-redact")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String autoRedactForm(Model model) {
|
public String autoRedactForm(Model model) {
|
||||||
model.addAttribute("currentPage", "auto-redact");
|
model.addAttribute("currentPage", "auto-redact");
|
||||||
return "security/auto-redact";
|
return "security/auto-redact";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/add-password")
|
@GetMapping("/add-password")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String addPasswordForm(Model model) {
|
public String addPasswordForm(Model model) {
|
||||||
model.addAttribute("currentPage", "add-password");
|
model.addAttribute("currentPage", "add-password");
|
||||||
return "security/add-password";
|
return "security/add-password";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/change-permissions")
|
@GetMapping("/change-permissions")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String permissionsForm(Model model) {
|
public String permissionsForm(Model model) {
|
||||||
@@ -44,21 +45,21 @@ public class SecurityWebController {
|
|||||||
model.addAttribute("currentPage", "add-watermark");
|
model.addAttribute("currentPage", "add-watermark");
|
||||||
return "security/add-watermark";
|
return "security/add-watermark";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/cert-sign")
|
@GetMapping("/cert-sign")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String certSignForm(Model model) {
|
public String certSignForm(Model model) {
|
||||||
model.addAttribute("currentPage", "cert-sign");
|
model.addAttribute("currentPage", "cert-sign");
|
||||||
return "security/cert-sign";
|
return "security/cert-sign";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/sanitize-pdf")
|
@GetMapping("/sanitize-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String sanitizeForm(Model model) {
|
public String sanitizeForm(Model model) {
|
||||||
model.addAttribute("currentPage", "sanitize-pdf");
|
model.addAttribute("currentPage", "sanitize-pdf");
|
||||||
return "security/sanitize-pdf";
|
return "security/sanitize-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/get-info-on-pdf")
|
@GetMapping("/get-info-on-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String getInfo(Model model) {
|
public String getInfo(Model model) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user