Compare commits
258 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c57b308909 | ||
|
|
6409274f83 | ||
|
|
bc534c12a5 | ||
|
|
b58fd2022a | ||
|
|
d850d026ed | ||
|
|
83627686d4 | ||
|
|
4e7d01c72c | ||
|
|
0f8ab20db7 | ||
|
|
88cc90786d | ||
|
|
fb66717b43 | ||
|
|
1d60433fcf | ||
|
|
0f3df6e92b | ||
|
|
ca7c63c7d7 | ||
|
|
135f9611df | ||
|
|
cfaaeebd4a | ||
|
|
09a0779180 | ||
|
|
7f7d09bc85 | ||
|
|
0c454a08dc | ||
|
|
d749b63549 | ||
|
|
2053a6950d | ||
|
|
41bd801e0d | ||
|
|
cd0e1a3962 | ||
|
|
7c2f482b3b | ||
|
|
7741d60afd | ||
|
|
8aac0c0327 | ||
|
|
7c26c56210 | ||
|
|
e88a780efe | ||
|
|
363fb5dc02 | ||
|
|
39a187b6da | ||
|
|
b4cc34a522 | ||
|
|
af94ef3d49 | ||
|
|
505855a53c | ||
|
|
87ac245341 | ||
|
|
1670a09d04 | ||
|
|
620b954336 | ||
|
|
cfd51e9b84 | ||
|
|
6c797f8216 | ||
|
|
40e208152a | ||
|
|
cf7bfa62ef | ||
|
|
a1086b9a04 | ||
|
|
9bc4bbd2c8 | ||
|
|
73156012e9 | ||
|
|
91cc3d77d4 | ||
|
|
3fc55a9e9f | ||
|
|
b666aa3f26 | ||
|
|
53e7dbe12f | ||
|
|
9d8ff6856b | ||
|
|
3fb38376b0 | ||
|
|
15c73d9dd3 | ||
|
|
86f71ffb93 | ||
|
|
eb928d3369 | ||
|
|
d7307665b3 | ||
|
|
2836f0ab5a | ||
|
|
91b7f3980c | ||
|
|
5053432c2d | ||
|
|
563c612395 | ||
|
|
95dced6455 | ||
|
|
989f0bbbfb | ||
|
|
e5eec28bfd | ||
|
|
bfc402f307 | ||
|
|
35a998b934 | ||
|
|
cadc8e499d | ||
|
|
7f7ea6da9f | ||
|
|
ab9a22d8e7 | ||
|
|
cd2728105e | ||
|
|
d75e84bdff | ||
|
|
e791fee38b | ||
|
|
ad5f057733 | ||
|
|
6f325b5fdb | ||
|
|
b73aeee18d | ||
|
|
8a54035a9f | ||
|
|
af28e30e4c | ||
|
|
492513306c | ||
|
|
bc554ff4a7 | ||
|
|
b7a0d1ece8 | ||
|
|
ca384218dc | ||
|
|
e9550fd6b2 | ||
|
|
232a305d51 | ||
|
|
a6ab448eeb | ||
|
|
f9aa157c6c | ||
|
|
b7df24acaa | ||
|
|
ad4ca1b2d7 | ||
|
|
c562d197e7 | ||
|
|
83ba1899b7 | ||
|
|
3420adc7c9 | ||
|
|
fd39f28e46 | ||
|
|
710125852a | ||
|
|
e8ec208390 | ||
|
|
4584562607 | ||
|
|
2c1412a088 | ||
|
|
0a65382979 | ||
|
|
4f404f66e5 | ||
|
|
89505ada00 | ||
|
|
374a30ac5a | ||
|
|
cee4ee4128 | ||
|
|
58fc5e2ffa | ||
|
|
951ea43f8b | ||
|
|
ca16ecef24 | ||
|
|
bb025dc2a1 | ||
|
|
d797169bd0 | ||
|
|
891f9e2252 | ||
|
|
4a579c00ce | ||
|
|
9cb4d8e088 | ||
|
|
5d3ee7755a | ||
|
|
c047c46587 | ||
|
|
54f53be5b5 | ||
|
|
8bb9e5b22f | ||
|
|
379791a326 | ||
|
|
38ec68b303 | ||
|
|
a5095b04ad | ||
|
|
a27ddb40be | ||
|
|
bc36be8a5e | ||
|
|
1e35556034 | ||
|
|
882cd41d4b | ||
|
|
724fb4bf8f | ||
|
|
b07437dbfa | ||
|
|
96f05cd518 | ||
|
|
77411e94a4 | ||
|
|
0da9c62ef8 | ||
|
|
52a7885f3c | ||
|
|
f98f089d63 | ||
|
|
6b618f3abe | ||
|
|
0732ffa76e | ||
|
|
b5b4636e56 | ||
|
|
ca12d040e1 | ||
|
|
3388b9fafa | ||
|
|
954b36e14c | ||
|
|
4d43814220 | ||
|
|
f6262c82e1 | ||
|
|
7ead12922f | ||
|
|
33a6a7869c | ||
|
|
bf995f989c | ||
|
|
21de6c6520 | ||
|
|
c14aa6851e | ||
|
|
8260eced2d | ||
|
|
d028465dc5 | ||
|
|
29dab5e47d | ||
|
|
9e655631b4 | ||
|
|
179c7b80bb | ||
|
|
349bf29122 | ||
|
|
295357f12b | ||
|
|
940f8d999e | ||
|
|
5605d53a5f | ||
|
|
116d103119 | ||
|
|
2fd8c643af | ||
|
|
4367ae7934 | ||
|
|
749461334d | ||
|
|
e83a027023 | ||
|
|
1883b477a3 | ||
|
|
37e2cd40da | ||
|
|
81a9329975 | ||
|
|
0eb019fc3c | ||
|
|
4129c75475 | ||
|
|
3d66f03f58 | ||
|
|
7b83104fd6 | ||
|
|
794aede27f | ||
|
|
08eb39b206 | ||
|
|
2566c7f3d7 | ||
|
|
a8522bb3b5 | ||
|
|
92b9142902 | ||
|
|
d07e3e6522 | ||
|
|
29aabdfba8 | ||
|
|
9af1b0cfdc | ||
|
|
6e32c7fe85 | ||
|
|
ddf5915c6a | ||
|
|
cdbf1fa73a | ||
|
|
5d926b022b | ||
|
|
50bcca10e2 | ||
|
|
a5528c06ee | ||
|
|
94526de04b | ||
|
|
1ddf7abe6f | ||
|
|
a742c1b034 | ||
|
|
6e726ac2a6 | ||
|
|
5877b40be5 | ||
|
|
a3c7f5aa46 | ||
|
|
4e28bf03bd | ||
|
|
f92482d89e | ||
|
|
3c54429fe0 | ||
|
|
4c4c22e861 | ||
|
|
00e27d9327 | ||
|
|
f082278041 | ||
|
|
09dde64c57 | ||
|
|
b52a6357f6 | ||
|
|
f14a566d06 | ||
|
|
b352ec6888 | ||
|
|
4ec1bad03d | ||
|
|
f20bbc119d | ||
|
|
8555c3d422 | ||
|
|
71f9d03b19 | ||
|
|
0b31379078 | ||
|
|
29c204b2c2 | ||
|
|
657e881963 | ||
|
|
ef6bdc70a4 | ||
|
|
e4a36115a2 | ||
|
|
325f86832c | ||
|
|
e3dbdd6b09 | ||
|
|
919041e879 | ||
|
|
182231a183 | ||
|
|
46d4ae8fc5 | ||
|
|
73ab1936a3 | ||
|
|
59c72527b5 | ||
|
|
5ea3bcc1dd | ||
|
|
c140052822 | ||
|
|
f7832774d9 | ||
|
|
279d25c03a | ||
|
|
f1984047a8 | ||
|
|
eae7c1bd60 | ||
|
|
d7f592ebda | ||
|
|
1798ce002a | ||
|
|
57a0cca595 | ||
|
|
b26fbd7693 | ||
|
|
43b0e25bdb | ||
|
|
3377af1305 | ||
|
|
c81c1006b7 | ||
|
|
f313857f96 | ||
|
|
d5fbe02149 | ||
|
|
159cee0b39 | ||
|
|
5da4dd6cca | ||
|
|
e9daf05f16 | ||
|
|
a12643194a | ||
|
|
25d8fc08f7 | ||
|
|
a72378dd4d | ||
|
|
9aed70408b | ||
|
|
5ae2c71c3a | ||
|
|
4edce515b8 | ||
|
|
67ff664eb8 | ||
|
|
71c1a4f102 | ||
|
|
ba4ba1b9fc | ||
|
|
59f10f06ca | ||
|
|
f7953cbc37 | ||
|
|
9a74e81754 | ||
|
|
420e4b6766 | ||
|
|
aed48ffc93 | ||
|
|
0cebe69ff8 | ||
|
|
e5990dba81 | ||
|
|
c9b0d01250 | ||
|
|
4918ed3f3c | ||
|
|
b176ce4251 | ||
|
|
518ff5409e | ||
|
|
803bd3c5b2 | ||
|
|
03d4e73304 | ||
|
|
55a820b09f | ||
|
|
f2a65dc360 | ||
|
|
7b4a889ea7 | ||
|
|
f627d251c3 | ||
|
|
d5b7125415 | ||
|
|
67dd3cf0e3 | ||
|
|
b8b62bb5af | ||
|
|
b4a9d1ac18 | ||
|
|
8aae651c2c | ||
|
|
fc9465b324 | ||
|
|
579a50be2c | ||
|
|
9c5b967e4c | ||
|
|
d41deb729b | ||
|
|
a93a89f3f0 | ||
|
|
11d642a25f | ||
|
|
67448498ea | ||
|
|
489b8da713 |
94
.github/workflows/push-docker.yml
vendored
94
.github/workflows/push-docker.yml
vendored
@@ -9,13 +9,6 @@ on:
|
||||
jobs:
|
||||
push:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
config: [
|
||||
{ dockerfile: "./Dockerfile", tagSuffix: "" },
|
||||
{ dockerfile: "./Dockerfile-ultra-lite", tagSuffix: "-ultra-lite" },
|
||||
{ dockerfile: "./Dockerfile-lite", tagSuffix: "-lite" }
|
||||
]
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v3.5.2
|
||||
@@ -28,6 +21,8 @@ jobs:
|
||||
|
||||
|
||||
- uses: gradle/gradle-build-action@v2.4.2
|
||||
env:
|
||||
DOCKER_ENABLE_SECURITY: false
|
||||
with:
|
||||
gradle-version: 7.6
|
||||
arguments: clean build
|
||||
@@ -56,14 +51,6 @@ jobs:
|
||||
id: repoowner
|
||||
run: echo "::set-output name=lowercase::$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')"
|
||||
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
|
||||
- name: Generate tags
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4.4.0
|
||||
@@ -72,19 +59,84 @@ jobs:
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||
tags: |
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}${{ matrix.config.tagSuffix }},enable=${{ github.ref == 'refs/heads/master' }}
|
||||
type=raw,value=latest${{ matrix.config.tagSuffix }},enable=${{ github.ref == 'refs/heads/master' }}
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' }}
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Build and push Dockerfile
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Build and push main Dockerfile
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
dockerfile: ${{ matrix.config.dockerfile }}
|
||||
dockerfile: ./Dockerfile
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args:
|
||||
VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
|
||||
|
||||
|
||||
- name: Generate tags ultra-lite
|
||||
id: meta2
|
||||
uses: docker/metadata-action@v4.4.0
|
||||
if: github.ref != 'refs/heads/main'
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||
tags: |
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
|
||||
|
||||
- name: Build and push Dockerfile-ultra-lite
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
if: github.ref != 'refs/heads/main'
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile-ultra-lite
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
tags: ${{ steps.meta2.outputs.tags }}
|
||||
labels: ${{ steps.meta2.outputs.labels }}
|
||||
build-args:
|
||||
VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
|
||||
|
||||
|
||||
- name: Generate tags lite
|
||||
id: meta3
|
||||
uses: docker/metadata-action@v4.4.0
|
||||
if: github.ref != 'refs/heads/main'
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||
tags: |
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
type=raw,value=latest-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
|
||||
|
||||
- name: Build and push Dockerfile-lite
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
if: github.ref != 'refs/heads/main'
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile-lite
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
tags: ${{ steps.meta3.outputs.tags }}
|
||||
labels: ${{ steps.meta3.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
|
||||
55
.github/workflows/releaseArtifacts.yml
vendored
Normal file
55
.github/workflows/releaseArtifacts.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: Release Artifacts
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
push:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
enable_security: [true, false]
|
||||
include:
|
||||
- enable_security: true
|
||||
file_suffix: '-with-login'
|
||||
- enable_security: false
|
||||
file_suffix: ''
|
||||
steps:
|
||||
- uses: actions/checkout@v3.5.2
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3.11.0
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Generate jar (With Security=${{ matrix.enable_security }})
|
||||
run: ./gradlew clean createExe
|
||||
env:
|
||||
DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }}
|
||||
|
||||
- name: Upload binaries to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ./build/launch4j/Stirling-PDF.exe
|
||||
asset_name: Stirling-PDF${{ matrix.file_suffix }}.exe
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: echo "::set-output name=versionNumber::$(./gradlew printVersion --quiet | tail -1)"
|
||||
|
||||
- name: Upload jar binaries to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar
|
||||
asset_name: Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
235
.gitignore
vendored
235
.gitignore
vendored
@@ -1,115 +1,122 @@
|
||||
|
||||
|
||||
### Eclipse ###
|
||||
.metadata
|
||||
bin/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
.settings/
|
||||
.loadpath
|
||||
.recommenders
|
||||
.classpath
|
||||
.project
|
||||
version.properties
|
||||
|
||||
# Gradle
|
||||
.gradle
|
||||
.lock
|
||||
|
||||
# External tool builders
|
||||
.externalToolBuilders/
|
||||
|
||||
# Locally stored "Eclipse launch configurations"
|
||||
*.launch
|
||||
|
||||
# PyDev specific (Python IDE for Eclipse)
|
||||
*.pydevproject
|
||||
|
||||
# CDT-specific (C/C++ Development Tooling)
|
||||
.cproject
|
||||
|
||||
# CDT- autotools
|
||||
.autotools
|
||||
|
||||
# Java annotation processor (APT)
|
||||
.factorypath
|
||||
|
||||
# PDT-specific (PHP Development Tools)
|
||||
.buildpath
|
||||
|
||||
# sbteclipse plugin
|
||||
.target
|
||||
|
||||
# Tern plugin
|
||||
.tern-project
|
||||
|
||||
# TeXlipse plugin
|
||||
.texlipse
|
||||
|
||||
# STS (Spring Tool Suite)
|
||||
.springBeans
|
||||
|
||||
# Code Recommenders
|
||||
.recommenders/
|
||||
|
||||
# Annotation Processing
|
||||
.apt_generated/
|
||||
.apt_generated_test/
|
||||
|
||||
# Scala IDE specific (Scala & Java development for Eclipse)
|
||||
.cache-main
|
||||
.scala_dependencies
|
||||
.worksheet
|
||||
|
||||
# Uncomment this line if you wish to ignore the project description file.
|
||||
# Typically, this file would be tracked if it contains build/dependency configurations:
|
||||
#.project
|
||||
|
||||
### Eclipse Patch ###
|
||||
# Spring Boot Tooling
|
||||
.sts4-cache/
|
||||
|
||||
### Git ###
|
||||
# Created by git for backups. To disable backups in Git:
|
||||
# $ git config --global mergetool.keepBackup false
|
||||
*.orig
|
||||
|
||||
# Created by git when using merge tools for conflicts
|
||||
*.BACKUP.*
|
||||
*.BASE.*
|
||||
*.LOCAL.*
|
||||
*.REMOTE.*
|
||||
*_BACKUP_*.txt
|
||||
*_BASE_*.txt
|
||||
*_LOCAL_*.txt
|
||||
*_REMOTE_*.txt
|
||||
|
||||
### Java ###
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
/build
|
||||
|
||||
|
||||
|
||||
### Eclipse ###
|
||||
.metadata
|
||||
bin/
|
||||
tmp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
.settings/
|
||||
.loadpath
|
||||
.recommenders
|
||||
.classpath
|
||||
.project
|
||||
version.properties
|
||||
pipeline/
|
||||
|
||||
#### Stirling-PDF Files ###
|
||||
customFiles/
|
||||
configs/
|
||||
watchedFolders/
|
||||
|
||||
|
||||
# Gradle
|
||||
.gradle
|
||||
.lock
|
||||
|
||||
# External tool builders
|
||||
.externalToolBuilders/
|
||||
|
||||
# Locally stored "Eclipse launch configurations"
|
||||
*.launch
|
||||
|
||||
# PyDev specific (Python IDE for Eclipse)
|
||||
*.pydevproject
|
||||
|
||||
# CDT-specific (C/C++ Development Tooling)
|
||||
.cproject
|
||||
|
||||
# CDT- autotools
|
||||
.autotools
|
||||
|
||||
# Java annotation processor (APT)
|
||||
.factorypath
|
||||
|
||||
# PDT-specific (PHP Development Tools)
|
||||
.buildpath
|
||||
|
||||
# sbteclipse plugin
|
||||
.target
|
||||
|
||||
# Tern plugin
|
||||
.tern-project
|
||||
|
||||
# TeXlipse plugin
|
||||
.texlipse
|
||||
|
||||
# STS (Spring Tool Suite)
|
||||
.springBeans
|
||||
|
||||
# Code Recommenders
|
||||
.recommenders/
|
||||
|
||||
# Annotation Processing
|
||||
.apt_generated/
|
||||
.apt_generated_test/
|
||||
|
||||
# Scala IDE specific (Scala & Java development for Eclipse)
|
||||
.cache-main
|
||||
.scala_dependencies
|
||||
.worksheet
|
||||
|
||||
# Uncomment this line if you wish to ignore the project description file.
|
||||
# Typically, this file would be tracked if it contains build/dependency configurations:
|
||||
#.project
|
||||
|
||||
### Eclipse Patch ###
|
||||
# Spring Boot Tooling
|
||||
.sts4-cache/
|
||||
|
||||
### Git ###
|
||||
# Created by git for backups. To disable backups in Git:
|
||||
# $ git config --global mergetool.keepBackup false
|
||||
*.orig
|
||||
|
||||
# Created by git when using merge tools for conflicts
|
||||
*.BACKUP.*
|
||||
*.BASE.*
|
||||
*.LOCAL.*
|
||||
*.REMOTE.*
|
||||
*_BACKUP_*.txt
|
||||
*_BASE_*.txt
|
||||
*_LOCAL_*.txt
|
||||
*_REMOTE_*.txt
|
||||
|
||||
### Java ###
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
*.db
|
||||
/build
|
||||
|
||||
/.vscode
|
||||
17
Dockerfile
17
Dockerfile
@@ -1,11 +1,22 @@
|
||||
# Build jbig2enc in a separate stage
|
||||
FROM frooodle/stirling-pdf-base:latest
|
||||
FROM frooodle/stirling-pdf-base:beta4
|
||||
|
||||
ARG VERSION_TAG
|
||||
ENV VERSION_TAG=$VERSION_TAG
|
||||
|
||||
ENV DOCKER_ENABLE_SECURITY=false
|
||||
|
||||
# Create scripts folder and copy local scripts
|
||||
RUN mkdir /scripts
|
||||
COPY ./scripts/* /scripts/
|
||||
|
||||
# Copy the application JAR file
|
||||
#Install fonts
|
||||
RUN mkdir /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/
|
||||
RUN fc-cache -f -v
|
||||
|
||||
# Always copy the JAR
|
||||
COPY build/libs/*.jar app.jar
|
||||
|
||||
# Expose the application port
|
||||
@@ -13,8 +24,6 @@ EXPOSE 8080
|
||||
|
||||
# Set environment variables
|
||||
ENV APP_HOME_NAME="Stirling PDF"
|
||||
#ENV APP_HOME_DESCRIPTION="Personal PDF Website!"
|
||||
#ENV APP_NAVBAR_NAME="Stirling PDF"
|
||||
|
||||
# Run the application
|
||||
RUN chmod +x /scripts/init.sh
|
||||
|
||||
@@ -13,11 +13,14 @@ RUN apt-get update && \
|
||||
# Copy the application JAR file
|
||||
COPY build/libs/*.jar app.jar
|
||||
|
||||
|
||||
|
||||
# Expose the application port
|
||||
EXPOSE 8080
|
||||
|
||||
# Set environment variables
|
||||
ENV GROUPS_TO_REMOVE=Python,OpenCV,OCRmyPDF
|
||||
ENV DOCKER_ENABLE_SECURITY=false
|
||||
|
||||
# Run the application
|
||||
CMD ["java", "-jar", "/app.jar"]
|
||||
|
||||
@@ -7,8 +7,11 @@ COPY build/libs/*.jar app.jar
|
||||
# Expose the application port
|
||||
EXPOSE 8080
|
||||
|
||||
|
||||
|
||||
# Set environment variables
|
||||
ENV GROUPS_TO_REMOVE=LibreOffice,CLI
|
||||
ENV GROUPS_TO_REMOVE=CLI
|
||||
ENV DOCKER_ENABLE_SECURITY=false
|
||||
|
||||
# Run the application
|
||||
CMD ["java", "-jar", "/app.jar"]
|
||||
|
||||
@@ -29,7 +29,7 @@ RUN apt-get update && \
|
||||
libjpeg-dev && \
|
||||
pip install --upgrade pip && \
|
||||
pip install --no-cache-dir \
|
||||
opencv-python-headless && \
|
||||
opencv-python-headless WeasyPrint && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Final stage: Copy necessary files from the previous stage
|
||||
|
||||
46
Endpoint-groups.md
Normal file
46
Endpoint-groups.md
Normal file
@@ -0,0 +1,46 @@
|
||||
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript |
|
||||
|---------------------|---------|---------|----------|-------|------|--------|--------|-------------|----------|----------|------------|
|
||||
| adjust-contrast | ✔️ | | | | | | | | | | ✔️ |
|
||||
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | |
|
||||
| crop | ✔️ | | | | | | | | | ✔️ | |
|
||||
| extract-page | ✔️ | | | | | | | | | ✔️ | |
|
||||
| merge-pdfs | ✔️ | | | | | | | | | ✔️ | |
|
||||
| multi-page-layout | ✔️ | | | | | | | | | ✔️ | |
|
||||
| pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ |
|
||||
| pdf-to-single-page | ✔️ | | | | | | | | | ✔️ | |
|
||||
| remove-pages | ✔️ | | | | | | | | | ✔️ | |
|
||||
| rotate-pdf | ✔️ | | | | | | | | | ✔️ | |
|
||||
| scale-pages | ✔️ | | | | | | | | | ✔️ | |
|
||||
| split-pdfs | ✔️ | | | | | | | | | ✔️ | |
|
||||
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
|
||||
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||
| pdf-to-img | | ✔️ | | | | | | | | ✔️ | |
|
||||
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
|
||||
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | |
|
||||
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||
| pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||
| pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||
| xlsx-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||
| add-password | | | ✔️ | | | | | | | ✔️ | |
|
||||
| add-watermark | | | ✔️ | | | | | | | ✔️ | |
|
||||
| cert-sign | | | ✔️ | | | | | | | ✔️ | |
|
||||
| change-permissions | | | ✔️ | | | | | | | ✔️ | |
|
||||
| remove-password | | | ✔️ | | | | | | | ✔️ | |
|
||||
| sanitize-pdf | | | ✔️ | | | | | | | ✔️ | |
|
||||
| add-image | | | | ✔️ | | | | | | ✔️ | |
|
||||
| add-page-numbers | | | | ✔️ | | | | | | ✔️ | |
|
||||
| auto-rename | | | | ✔️ | | | | | | ✔️ | |
|
||||
| change-metadata | | | | ✔️ | | | | | | ✔️ | |
|
||||
| compare | | | | ✔️ | | | | | | | ✔️ |
|
||||
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
|
||||
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
|
||||
| extract-images | | | | ✔️ | | | | | | ✔️ | |
|
||||
| flatten | | | | ✔️ | | | | | | | ✔️ |
|
||||
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | |
|
||||
| ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
|
||||
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
|
||||
| repair | | | | ✔️ | ✔️ | | | ✔️ | | | |
|
||||
| show-javascript | | | | ✔️ | | | | | | | ✔️ |
|
||||
| sign | | | | ✔️ | | | | | | | ✔️ |
|
||||
@@ -3,7 +3,7 @@
|
||||
This document provides instructions on how to add additional language packs for the OCR tab in Stirling-PDF, both inside and outside of Docker.
|
||||
|
||||
## How does the OCR Work
|
||||
Stirling-PDF uses OCRmyPDF which in turn uses tesseract for its text recognition.
|
||||
Stirling-PDF uses [OCRmyPDF](https://github.com/ocrmypdf/OCRmyPDF) which in turn uses tesseract for its text recognition.
|
||||
All credit goes to them for this awesome work!
|
||||
|
||||
## Language Packs
|
||||
|
||||
@@ -123,7 +123,7 @@ This folder is required for the python scripts using OpenCV
|
||||
|
||||
```bash
|
||||
sudo mkdir /opt/Stirling-PDF &&\
|
||||
sudo mv ./build/libs/S-PDF-*.jar /opt/Stirling-PDF/ &&\
|
||||
sudo mv ./build/libs/Stirling-PDF-*.jar /opt/Stirling-PDF/ &&\
|
||||
sudo mv scripts /opt/Stirling-PDF/ &&\
|
||||
echo "Scripts installed."
|
||||
```
|
||||
|
||||
75
README.md
75
README.md
@@ -8,6 +8,8 @@
|
||||
[](https://www.paypal.com/paypalme/froodleplex)
|
||||
[](https://github.com/sponsors/Frooodle)
|
||||
|
||||
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Frooodle/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
||||
|
||||
This is a powerful locally hosted web based PDF manipulation tool using docker that allows you to perform various operations on PDF files, such as splitting merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application started as a 100% ChatGPT-made application and has evolved to include a wide range of features to handle all your PDF needs.
|
||||
|
||||
Stirling PDF makes no outbound calls for any record keeping or tracking.
|
||||
@@ -27,6 +29,11 @@ Feel free to request any features or bug fixes either in github issues or our [D
|
||||
- Convert PDFs to and from images
|
||||
- Reorganize PDF pages into different orders.
|
||||
- Add/Generate signatures
|
||||
- Format PDFs into a multi-paged page
|
||||
- Scale page contents size by set %
|
||||
- Adjust Contrast
|
||||
- Crop PDF
|
||||
- Auto Split PDF (With physically scanned page dividers)
|
||||
- Flatten PDFs
|
||||
- Repair PDFs
|
||||
- Detect and remove blank pages
|
||||
@@ -39,8 +46,14 @@ Feel free to request any features or bug fixes either in github issues or our [D
|
||||
- Add watermark(s)
|
||||
- Convert Any common file to PDF (using LibreOffice)
|
||||
- Convert PDF to Word/Powerpoint/Others (using LibreOffice)
|
||||
- Convert HTML to PDF
|
||||
- URL to PDF
|
||||
- Extract images from PDF
|
||||
- Extract images from Scans
|
||||
- Add page numbers
|
||||
- Auto rename file by detecting PDF header text
|
||||
- OCR on PDF (Using OCRMyPDF)
|
||||
- PDF/A conversion (Using OCRMyPDF)
|
||||
- Edit metadata
|
||||
- Dark mode support.
|
||||
- Custom download options (see [here](https://github.com/Frooodle/Stirling-PDF/blob/main/images/settings.png) for example)
|
||||
@@ -53,6 +66,7 @@ Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) h
|
||||
## Technologies used
|
||||
- Spring Boot + Thymeleaf
|
||||
- PDFBox
|
||||
- IText7
|
||||
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
|
||||
- [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF)
|
||||
- HTML, CSS, JavaScript
|
||||
@@ -68,16 +82,25 @@ Please view https://github.com/Frooodle/Stirling-PDF/blob/main/LocalRunGuide.md
|
||||
### Docker
|
||||
https://hub.docker.com/r/frooodle/s-pdf
|
||||
|
||||
Stirling PDF has 3 different versions, a Full version, Lite and ultra-Lite. Depending on the types of features you use you may want a smaller image to save on space.
|
||||
To see what the different versions offer please look at our [version mapping](https://github.com/Frooodle/Stirling-PDF/blob/main/Version-groups.md)
|
||||
For people that dont mind about space optimisation just use latest tag.
|
||||

|
||||

|
||||

|
||||
|
||||
Docker Run
|
||||
```
|
||||
docker run -d \
|
||||
-p 8080:8080 \
|
||||
-v /location/of/trainingData:/usr/share/tesseract-ocr/4.00/tessdata \
|
||||
--name stirling-pdf \
|
||||
frooodle/s-pdf
|
||||
frooodle/s-pdf:latest
|
||||
|
||||
|
||||
Can also add these for customisation but are not required
|
||||
-v /location/of/extraConfigs:/configs \
|
||||
-v /location/of/customFiles:/customFiles \
|
||||
-e APP_HOME_NAME="Stirling PDF" \
|
||||
-e APP_HOME_DESCRIPTION="Your locally hosted one-stop-shop for all your PDF needs." \
|
||||
-e APP_NAVBAR_NAME="Stirling PDF" \
|
||||
@@ -90,12 +113,13 @@ Docker Compose
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf:
|
||||
image: frooodle/s-pdf
|
||||
image: frooodle/s-pdf:latest
|
||||
ports:
|
||||
- '8080:8080'
|
||||
volumes:
|
||||
- /location/of/trainingData:/usr/share/tesseract-ocr/4.00/tessdata #Required for extra OCR languages
|
||||
# - /location/of/extraConfigs:/configs
|
||||
# - /location/of/customFiles:/customFiles/
|
||||
# environment:
|
||||
# APP_LOCALE: en_GB
|
||||
# APP_HOME_NAME: Stirling PDF
|
||||
@@ -111,7 +135,7 @@ services:
|
||||
Please view https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md
|
||||
|
||||
## Want to add your own language?
|
||||
Stirling PDF currently supports
|
||||
Stirling PDF currently supports 16!
|
||||
- English (English) (en_GB)
|
||||
- Arabic (العربية) (ar_AR)
|
||||
- German (Deutsch) (de_DE)
|
||||
@@ -124,6 +148,10 @@ Stirling PDF currently supports
|
||||
- Polish (Polski) (pl_PL)
|
||||
- Romanian (Română) (ro_RO)
|
||||
- Korean (한국어) (ko_KR)
|
||||
- Portuguese Brazilian (Português) (pt_BR)
|
||||
- Russian (Русский) (ru_RU)
|
||||
- Basque (Euskara) (eu_ES)
|
||||
- Japanese (日本語) (ja_JP)
|
||||
|
||||
If you want to add your own language to Stirling-PDF please refer
|
||||
https://github.com/Frooodle/Stirling-PDF/blob/main/HowToAddNewLanguage.md
|
||||
@@ -148,28 +176,43 @@ Using the same method you can also change
|
||||
- Enable/Disable search engine visiblility with ALLOW_GOOGLE_VISIBILITY with true / false values. Default disable visiblility.
|
||||
- Change root URI for Stirling-PDF ie change server.com/ to server.com/pdf-app by running APP_ROOT_PATH as pdf-app
|
||||
- Disable and remove endpoints and functionality from Stirling-PDF. Currently the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma seperated lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image to pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Frooodle/Stirling-PDF/blob/main/groups.md)
|
||||
|
||||
- Change the max file size allowed through the server with the environment variable MAX_FILE_SIZE. default 2000MB
|
||||
- Customise static files such as app logo by placing files in the /customFiles/static/ directory. Example to customise app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF
|
||||
- Enable/Disable metric api endpoints with ENABLE_API_METRICS. Default enabled
|
||||
|
||||
## API
|
||||
For those wanting to use Stirling-PDFs backend API to link with their own custom scripting to edit PDFs you can view all existing API documentation
|
||||
[here](https://app.swaggerhub.com/apis-docs/Frooodle/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation
|
||||
[here](https://app.swaggerhub.com/apis-docs/Frooodle/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF)
|
||||
|
||||
|
||||
## Login authentication (CURRENTLY ALPHA TAG ONLY)
|
||||
### Prerequisites:
|
||||
- User must have the folder ./configs volumed within docker so that it is retained during updates.
|
||||
- The environment variable 'login.enabled' must be set to true
|
||||
- The environment variables "INITIAL_USERNAME" and "INITIAL_PASSWORD" must also be populated (only required on first boot to create initial user, ignored after.)
|
||||
|
||||
Once the above has been done, on restart a new stirling-pdf-DB.mv.db will show if everything worked.
|
||||
|
||||
When you login to Stirling PDF you will be redirected to /login page to login with those credentials. After login everything should function as normal
|
||||
|
||||
To access your account settings go to Account settings in the settings cog menu (top right in navbar) this Account settings menu is also where you find your API key.
|
||||
|
||||
To add new users go to bottom of Account settings and hit 'Admin Settings', here you can add new users. The different roles mentioned within this are for rate limiting. This is a Work in progress which will be expanding on more in future
|
||||
|
||||
For API usage you must provide a header with 'X-API-Key' and the associated API key for that user.
|
||||
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q1: Can you add authentication in Stirling PDF?
|
||||
There is no Auth within Stirling PDF and there is none planned. This feature will not be added. Instead we recommended you use trusted and secure authentication software like Authentik or Authelia.
|
||||
|
||||
### Q2: What are your planned features?
|
||||
- Crop
|
||||
### Q1: What are your planned features?
|
||||
- Progress bar/Tracking
|
||||
- Full custom logic pipelines to combine multiple operations together.
|
||||
- Folder support with auto scanning to perform operations on
|
||||
- Redact sections of pages
|
||||
- Add page numbers
|
||||
- Auto rename (Renames file based on file title text)
|
||||
- URL to PDF
|
||||
- Change contrast
|
||||
- Redact text (Via UI)
|
||||
- Add Forms
|
||||
- Annotations
|
||||
- Multi page layout (Stich PDF pages together) support x rows y columns and custom page sizing
|
||||
- Fill forms mannual and automatic
|
||||
|
||||
### Q3: 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.
|
||||
|
||||
59
Version-groups.md
Normal file
59
Version-groups.md
Normal file
@@ -0,0 +1,59 @@
|
||||
|Technology | Ultra-Lite | Lite | Full |
|
||||
|----------------|:----------:|:----:|:----:|
|
||||
| Java | ✔️ | ✔️ | ✔️ |
|
||||
| JavaScript | ✔️ | ✔️ | ✔️ |
|
||||
| Libre | | ✔️ | ✔️ |
|
||||
| Python | | | ✔️ |
|
||||
| OpenCV | | | ✔️ |
|
||||
| OCRmyPDF | | | ✔️ |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Operation | Ultra-Lite | Lite | Full
|
||||
--------------------|------------|------|-----
|
||||
add-page-numbers | ✔️ | ✔️ | ✔️
|
||||
add-password | ✔️ | ✔️ | ✔️
|
||||
add-image | ✔️ | ✔️ | ✔️
|
||||
add-watermark | ✔️ | ✔️ | ✔️
|
||||
adjust-contrast | ✔️ | ✔️ | ✔️
|
||||
auto-split-pdf | ✔️ | ✔️ | ✔️
|
||||
auto-rename | ✔️ | ✔️ | ✔️
|
||||
cert-sign | ✔️ | ✔️ | ✔️
|
||||
crop | ✔️ | ✔️ | ✔️
|
||||
change-metadata | ✔️ | ✔️ | ✔️
|
||||
change-permissions | ✔️ | ✔️ | ✔️
|
||||
compare | ✔️ | ✔️ | ✔️
|
||||
extract-page | ✔️ | ✔️ | ✔️
|
||||
extract-images | ✔️ | ✔️ | ✔️
|
||||
flatten | ✔️ | ✔️ | ✔️
|
||||
get-info-on-pdf | ✔️ | ✔️ | ✔️
|
||||
img-to-pdf | ✔️ | ✔️ | ✔️
|
||||
markdown-to-pdf | ✔️ | ✔️ | ✔️
|
||||
merge-pdfs | ✔️ | ✔️ | ✔️
|
||||
multi-page-layout | ✔️ | ✔️ | ✔️
|
||||
pdf-organizer | ✔️ | ✔️ | ✔️
|
||||
pdf-to-img | ✔️ | ✔️ | ✔️
|
||||
pdf-to-single-page | ✔️ | ✔️ | ✔️
|
||||
remove-pages | ✔️ | ✔️ | ✔️
|
||||
remove-password | ✔️ | ✔️ | ✔️
|
||||
rotate-pdf | ✔️ | ✔️ | ✔️
|
||||
sanitize-pdf | ✔️ | ✔️ | ✔️
|
||||
scale-pages | ✔️ | ✔️ | ✔️
|
||||
sign | ✔️ | ✔️ | ✔️
|
||||
show-javascript | ✔️ | ✔️ | ✔️
|
||||
split-pdfs | ✔️ | ✔️ | ✔️
|
||||
file-to-pdf | | ✔️ | ✔️
|
||||
pdf-to-html | | ✔️ | ✔️
|
||||
pdf-to-presentation | | ✔️ | ✔️
|
||||
pdf-to-text | | ✔️ | ✔️
|
||||
pdf-to-word | | ✔️ | ✔️
|
||||
pdf-to-xml | | ✔️ | ✔️
|
||||
repair | | ✔️ | ✔️
|
||||
xlsx-to-pdf | | ✔️ | ✔️
|
||||
compress-pdf | | | ✔️
|
||||
extract-image-scans | | | ✔️
|
||||
ocr-pdf | | | ✔️
|
||||
pdf-to-pdfa | | | ✔️
|
||||
remove-blanks | | | ✔️
|
||||
77
build.gradle
77
build.gradle
@@ -1,42 +1,101 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '3.1.0'
|
||||
id 'io.spring.dependency-management' version '1.1.0'
|
||||
id 'org.springframework.boot' version '3.1.2'
|
||||
id 'io.spring.dependency-management' version '1.1.2'
|
||||
id 'org.springdoc.openapi-gradle-plugin' version '1.6.0'
|
||||
id "io.swagger.swaggerhub" version "1.1.0"
|
||||
id "io.swagger.swaggerhub" version "1.2.0"
|
||||
id 'edu.sc.seis.launch4j' version '3.0.3'
|
||||
}
|
||||
|
||||
group = 'stirling.software'
|
||||
version = '0.10.1'
|
||||
version = '0.13.0'
|
||||
sourceCompatibility = '17'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
if (System.getenv('DOCKER_ENABLE_SECURITY') == 'false') {
|
||||
exclude 'stirling/software/SPDF/config/security/**'
|
||||
exclude 'stirling/software/SPDF/controller/api/UserController.java'
|
||||
exclude 'stirling/software/SPDF/controller/web/AccountWebController.java'
|
||||
exclude 'stirling/software/SPDF/model/ApiKeyAuthenticationToken.java'
|
||||
exclude 'stirling/software/SPDF/model/Authority.java'
|
||||
exclude 'stirling/software/SPDF/model/PersistentLogin.java'
|
||||
exclude 'stirling/software/SPDF/model/User.java'
|
||||
exclude 'stirling/software/SPDF/repository/**'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
openApi {
|
||||
apiDocsUrl = "http://localhost:8080/v3/api-docs"
|
||||
outputDir = file("$projectDir")
|
||||
outputFileName = "SwaggerDoc.json"
|
||||
}
|
||||
|
||||
|
||||
launch4j {
|
||||
icon = "${projectDir}/src/main/resources/static/favicon.ico"
|
||||
|
||||
outfile="Stirling-PDF.exe"
|
||||
headerType="console"
|
||||
jarTask = tasks.bootJar
|
||||
|
||||
errTitle="Encountered error, Do you have Java 17?"
|
||||
downloadUrl="https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe"
|
||||
variables=["BROWSER_OPEN=true"]
|
||||
jreMinVersion="17"
|
||||
|
||||
mutexName="Stirling-PDF"
|
||||
windowTitle="Stirling-PDF"
|
||||
|
||||
messagesStartupError="An error occurred while starting Stirling-PDF"
|
||||
//messagesJreNotFoundError="This application requires a Java Runtime Environment, Please download Java 17."
|
||||
messagesJreVersionError="You are running the wrong version of Java, Please download Java 17."
|
||||
messagesLauncherError="Java is corrupted. Please uninstall and then install Java 17."
|
||||
messagesInstanceAlreadyExists="Stirling-PDF is already running."
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web:3.1.0'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.1.0'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.1.0'
|
||||
implementation 'org.yaml:snakeyaml:2.1'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web:3.1.2'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.1.2'
|
||||
|
||||
if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security:3.1.2'
|
||||
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE'
|
||||
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
|
||||
implementation "com.h2database:h2"
|
||||
}
|
||||
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.1.2'
|
||||
|
||||
|
||||
|
||||
// https://mvnrepository.com/artifact/org.apache.pdfbox/jbig2-imageio
|
||||
implementation group: 'org.apache.pdfbox', name: 'jbig2-imageio', version: '3.0.4'
|
||||
implementation 'commons-io:commons-io:2.11.0'
|
||||
implementation 'commons-io:commons-io:2.13.0'
|
||||
|
||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
|
||||
|
||||
//general PDF
|
||||
implementation 'org.apache.pdfbox:pdfbox:2.0.28'
|
||||
implementation 'org.apache.pdfbox:pdfbox:2.0.29'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
|
||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
|
||||
implementation 'com.itextpdf:itext7-core:7.2.5'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
implementation 'io.micrometer:micrometer-core'
|
||||
implementation group: 'com.google.zxing', name: 'core', version: '3.5.1'
|
||||
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
||||
implementation 'org.commonmark:commonmark:0.21.0'
|
||||
// https://mvnrepository.com/artifact/com.github.vladimir-bukhtoyarov/bucket4j-core
|
||||
implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0'
|
||||
|
||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||
|
||||
|
||||
38
groups.md
38
groups.md
@@ -1,38 +0,0 @@
|
||||
Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript
|
||||
--------------------|---------|---------|----------|-------|------|--------|--------|-------------|--------- |-------- |-----------
|
||||
remove-pages | X | | | | | | | | | X |
|
||||
merge-pdfs | X | | | | | | | | | X |
|
||||
split-pdfs | X | | | | | | | | | X |
|
||||
pdf-organizer | X | | | | | | | | | X | X
|
||||
rotate-pdf | X | | | | | | | | | X |
|
||||
multi-page-layout | X | | | | | | | | | X |
|
||||
scale-pages | X | | | | | | | | | X |
|
||||
pdf-to-img | | X | | | | | | | | X |
|
||||
img-to-pdf | | X | | | | | | | | X |
|
||||
pdf-to-pdfa | | X | | | X | | | | X | |
|
||||
file-to-pdf | | X | | | X | | | X | | |
|
||||
xlsx-to-pdf | | X | | | X | | | X | | |
|
||||
pdf-to-word | | X | | | X | | | X | | |
|
||||
pdf-to-presentation | | X | | | X | | | X | | |
|
||||
pdf-to-text | | X | | | X | | | X | | |
|
||||
pdf-to-html | | X | | | X | | | X | | |
|
||||
pdf-to-xml | | X | | | X | | | X | | |
|
||||
add-password | | | X | | | | | | | X |
|
||||
remove-password | | | X | | | | | | | X |
|
||||
change-permissions | | | X | | | | | | | X |
|
||||
add-watermark | | | X | | | | | | | X |
|
||||
cert-sign | | | X | | | | | | | X |
|
||||
ocr-pdf | | | | X | X | | | | X | |
|
||||
add-image | | | | X | | | | | | X |
|
||||
compress-pdf | | | | X | X | | | | X
|
||||
extract-images | | | | X | | | | | | X |
|
||||
change-metadata | | | | X | | | | | | X |
|
||||
extract-image-scans | | | | X | X | X | X | | | |
|
||||
sign | | | | X | | | | | | | X
|
||||
flatten | | | | X | | | | | | |
|
||||
repair | | | | X | X | | | X | | |
|
||||
remove-blanks | | | | X | X | X | X | | | |
|
||||
compare | | | | X | | | | | | | X
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 118 KiB |
36
lauch4jConfig.xml
Normal file
36
lauch4jConfig.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<launch4jConfig>
|
||||
<dontWrapJar>false</dontWrapJar>
|
||||
<headerType>console</headerType>
|
||||
<jar>.\build\libs\S-PDF-0.10.1.jar</jar>
|
||||
<outfile>.\Stirling-PDF.exe</outfile>
|
||||
<errTitle>Please download Java17</errTitle>
|
||||
<cmdLine></cmdLine>
|
||||
<chdir>.</chdir>
|
||||
<priority>normal</priority>
|
||||
<downloadUrl>https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe</downloadUrl>
|
||||
<supportUrl></supportUrl>
|
||||
<stayAlive>false</stayAlive>
|
||||
<restartOnCrash>false</restartOnCrash>
|
||||
<manifest></manifest>
|
||||
<icon>./src/main/resources/static/favicon.ico</icon>
|
||||
<var>BROWSER_OPEN=true</var>
|
||||
<singleInstance>
|
||||
<mutexName>Stirling-PDF</mutexName>
|
||||
<windowTitle>Stirling-PDF</windowTitle>
|
||||
</singleInstance>
|
||||
<jre>
|
||||
<path>%JAVA_HOME%;%PATH%</path>
|
||||
<requiresJdk>false</requiresJdk>
|
||||
<requires64Bit>false</requires64Bit>
|
||||
<minVersion>17</minVersion>
|
||||
<maxVersion></maxVersion>
|
||||
</jre>
|
||||
<messages>
|
||||
<startupErr>An error occurred while starting Stirling-PDF</startupErr>
|
||||
<jreNotFoundErr>This application requires a Java Runtime Environment, Please download Java 17.</jreNotFoundErr>
|
||||
<jreVersionErr>You are running the wrong version of Java, Please download Java 17.</jreVersionErr>
|
||||
<launcherErr>Java is corrupted. Please uninstall and then install Java 17.</launcherErr>
|
||||
<instanceAlreadyExistsMsg>Stirling-PDF is already running.</instanceAlreadyExistsMsg>
|
||||
</messages>
|
||||
</launch4jConfig>
|
||||
80
scripts/PropSync.java
Normal file
80
scripts/PropSync.java
Normal file
@@ -0,0 +1,80 @@
|
||||
package stirling.software.Stirling.Stats;
|
||||
|
||||
import java.nio.file.*;
|
||||
import java.nio.charset.MalformedInputException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
public class PropSync {
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
File folder = new File("C:\\Users\\systo\\git\\Stirling-PDF\\src\\main\\resources");
|
||||
File[] files = folder.listFiles((dir, name) -> name.matches("messages_.*\\.properties"));
|
||||
|
||||
List<String> enLines = Files.readAllLines(Paths.get(folder + "\\messages_en_GB.properties"), StandardCharsets.UTF_8);
|
||||
Map<String, String> enProps = linesToProps(enLines);
|
||||
|
||||
for (File file : files) {
|
||||
if (!file.getName().equals("messages_en_GB.properties")) {
|
||||
System.out.println("Processing file: " + file.getName());
|
||||
List<String> lines;
|
||||
try {
|
||||
lines = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
|
||||
} catch (MalformedInputException e) {
|
||||
System.out.println("Skipping due to not UTF8 format for file: " + file.getName());
|
||||
continue;
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
|
||||
Map<String, String> currentProps = linesToProps(lines);
|
||||
List<String> newLines = syncPropsWithLines(enProps, currentProps, enLines);
|
||||
|
||||
Files.write(file.toPath(), newLines, StandardCharsets.UTF_8);
|
||||
System.out.println("Finished processing file: " + file.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, String> linesToProps(List<String> lines) {
|
||||
Map<String, String> props = new LinkedHashMap<>();
|
||||
for (String line : lines) {
|
||||
if (!line.trim().isEmpty() && line.contains("=")) {
|
||||
String[] parts = line.split("=", 2);
|
||||
props.put(parts[0].trim(), parts[1].trim());
|
||||
}
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
private static List<String> syncPropsWithLines(Map<String, String> enProps, Map<String, String> currentProps, List<String> enLines) {
|
||||
List<String> newLines = new ArrayList<>();
|
||||
boolean needsTranslateComment = false; // flag to check if we need to add "TODO: Translate"
|
||||
|
||||
for (String line : enLines) {
|
||||
if (line.contains("=")) {
|
||||
String key = line.split("=", 2)[0].trim();
|
||||
|
||||
if (currentProps.containsKey(key)) {
|
||||
newLines.add(key + "=" + currentProps.get(key));
|
||||
needsTranslateComment = false;
|
||||
} else {
|
||||
if (!needsTranslateComment) {
|
||||
newLines.add("##########################");
|
||||
newLines.add("### TODO: Translate ###");
|
||||
newLines.add("##########################");
|
||||
needsTranslateComment = true;
|
||||
}
|
||||
newLines.add(line);
|
||||
}
|
||||
} else {
|
||||
// handle comments and other non-property lines
|
||||
newLines.add(line);
|
||||
needsTranslateComment = false; // reset the flag when we encounter comments or empty lines
|
||||
}
|
||||
}
|
||||
|
||||
return newLines;
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,27 @@ echo "Copying original files without overwriting existing files"
|
||||
mkdir -p /usr/share/tesseract-ocr
|
||||
cp -rn /usr/share/tesseract-ocr-original/* /usr/share/tesseract-ocr
|
||||
|
||||
# Check if TESSERACT_LANGS environment variable is set and is not empty
|
||||
if [[ -n "$TESSERACT_LANGS" ]]; then
|
||||
# Convert comma-separated values to a space-separated list
|
||||
LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ')
|
||||
|
||||
# Install each language pack
|
||||
for LANG in $LANGS; do
|
||||
apt-get install -y "tesseract-ocr-$LANG"
|
||||
done
|
||||
fi
|
||||
|
||||
# Check for DOCKER_ENABLE_SECURITY and download the appropriate JAR if required
|
||||
if [ "$DOCKER_ENABLE_SECURITY" = "true" ] && [ "$VERSION_TAG" != "alpha" ]; then
|
||||
echo "Downloading from: https://github.com/Frooodle/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar"
|
||||
curl -L -o new-app.jar https://github.com/Frooodle/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar
|
||||
if [ $? -eq 0 ]; then # checks if curl was successful
|
||||
rm -f app.jar
|
||||
mv new-app.jar app.jar
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
# Run the main command
|
||||
exec "$@"
|
||||
@@ -1 +1 @@
|
||||
rootProject.name = 'S-PDF'
|
||||
rootProject.name = 'Stirling-PDF'
|
||||
|
||||
@@ -1,11 +1,84 @@
|
||||
package stirling.software.SPDF;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SPdfApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SPdfApplication.class, args);
|
||||
}
|
||||
package stirling.software.SPDF;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import stirling.software.SPDF.config.ConfigInitializer;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
@SpringBootApplication
|
||||
|
||||
//@EnableScheduling
|
||||
public class SPdfApplication {
|
||||
|
||||
@Autowired
|
||||
private Environment env;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// Check if the BROWSER_OPEN environment variable is set to true
|
||||
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
|
||||
boolean browserOpen = browserOpenEnv != null && browserOpenEnv.equalsIgnoreCase("true");
|
||||
|
||||
if (browserOpen) {
|
||||
try {
|
||||
String port = env.getProperty("local.server.port");
|
||||
if(port == null || port.length() == 0) {
|
||||
port="8080";
|
||||
}
|
||||
String url = "http://localhost:" + port;
|
||||
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
Runtime rt = Runtime.getRuntime();
|
||||
if (os.contains("win")) {
|
||||
// For Windows
|
||||
rt.exec("rundll32 url.dll,FileProtocolHandler " + url);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication app = new SpringApplication(SPdfApplication.class);
|
||||
app.addInitializers(new ConfigInitializer());
|
||||
if (Files.exists(Paths.get("configs/settings.yml"))) {
|
||||
app.setDefaultProperties(Collections.singletonMap("spring.config.location", "file:configs/settings.yml"));
|
||||
} else {
|
||||
System.out.println("External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
|
||||
}
|
||||
app.run(args);
|
||||
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
GeneralUtils.createDir("customFiles/static/");
|
||||
GeneralUtils.createDir("customFiles/templates/");
|
||||
GeneralUtils.createDir("config");
|
||||
|
||||
|
||||
|
||||
System.out.println("Stirling-PDF Started.");
|
||||
|
||||
String port = System.getProperty("local.server.port");
|
||||
if(port == null || port.length() == 0) {
|
||||
port="8080";
|
||||
}
|
||||
String url = "http://localhost:" + port;
|
||||
System.out.println("Navigate to " + url);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,42 +1,54 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class AppConfig {
|
||||
@Bean(name = "appName")
|
||||
public String appName() {
|
||||
String appName = System.getProperty("APP_HOME_NAME");
|
||||
if (appName == null)
|
||||
appName = System.getenv("APP_HOME_NAME");
|
||||
return (appName != null) ? appName : "Stirling PDF";
|
||||
}
|
||||
|
||||
@Bean(name = "appVersion")
|
||||
public String appVersion() {
|
||||
String version = getClass().getPackage().getImplementationVersion();
|
||||
return (version != null) ? version : "0.0.0";
|
||||
}
|
||||
|
||||
@Bean(name = "homeText")
|
||||
public String homeText() {
|
||||
String homeText = System.getProperty("APP_HOME_DESCRIPTION");
|
||||
if (homeText == null)
|
||||
homeText = System.getenv("APP_HOME_DESCRIPTION");
|
||||
return (homeText != null) ? homeText : "null";
|
||||
}
|
||||
|
||||
@Bean(name = "navBarText")
|
||||
public String navBarText() {
|
||||
String navBarText = System.getProperty("APP_NAVBAR_NAME");
|
||||
if (navBarText == null)
|
||||
navBarText = System.getenv("APP_NAVBAR_NAME");
|
||||
if (navBarText == null)
|
||||
navBarText = System.getProperty("APP_HOME_NAME");
|
||||
if (navBarText == null)
|
||||
navBarText = System.getenv("APP_HOME_NAME");
|
||||
|
||||
return (navBarText != null) ? navBarText : "Stirling PDF";
|
||||
}
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
@Configuration
|
||||
public class AppConfig {
|
||||
|
||||
@Autowired
|
||||
ApplicationProperties applicationProperties;
|
||||
|
||||
@Bean(name = "loginEnabled")
|
||||
public boolean loginEnabled() {
|
||||
System.out.println(applicationProperties.toString());
|
||||
return applicationProperties.getSecurity().getEnableLogin();
|
||||
}
|
||||
|
||||
@Bean(name = "appName")
|
||||
public String appName() {
|
||||
String homeTitle = applicationProperties.getUi().getAppName();
|
||||
return (homeTitle != null) ? homeTitle : "Stirling PDF";
|
||||
}
|
||||
|
||||
@Bean(name = "appVersion")
|
||||
public String appVersion() {
|
||||
String version = getClass().getPackage().getImplementationVersion();
|
||||
return (version != null) ? version : "0.0.0";
|
||||
}
|
||||
|
||||
@Bean(name = "homeText")
|
||||
public String homeText() {
|
||||
return (applicationProperties.getUi().getHomeDescription() != null) ? applicationProperties.getUi().getHomeDescription() : "null";
|
||||
}
|
||||
|
||||
|
||||
@Bean(name = "navBarText")
|
||||
public String navBarText() {
|
||||
String defaultNavBar = applicationProperties.getUi().getAppNameNavbar() != null ? applicationProperties.getUi().getAppNameNavbar() : applicationProperties.getUi().getAppName();
|
||||
return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF";
|
||||
}
|
||||
|
||||
@Bean(name = "rateLimit")
|
||||
public boolean rateLimit() {
|
||||
String appName = System.getProperty("rateLimit");
|
||||
if (appName == null)
|
||||
appName = System.getenv("rateLimit");
|
||||
System.out.println("rateLimit=" + appName);
|
||||
return (appName != null) ? Boolean.valueOf(appName) : false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,53 +1,65 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.LocaleResolver;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
|
||||
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
|
||||
|
||||
@Configuration
|
||||
public class Beans implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(localeChangeInterceptor());
|
||||
registry.addInterceptor(new CleanUrlInterceptor());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LocaleChangeInterceptor localeChangeInterceptor() {
|
||||
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
|
||||
lci.setParamName("lang");
|
||||
return lci;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LocaleResolver localeResolver() {
|
||||
SessionLocaleResolver slr = new SessionLocaleResolver();
|
||||
|
||||
String appLocaleEnv = System.getProperty("APP_LOCALE");
|
||||
if (appLocaleEnv == null)
|
||||
appLocaleEnv = System.getenv("APP_LOCALE");
|
||||
Locale defaultLocale = Locale.UK; // Fallback to UK locale if environment variable is not set
|
||||
|
||||
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
|
||||
Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
|
||||
String tempLanguageTag = tempLocale.toLanguageTag();
|
||||
|
||||
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
|
||||
defaultLocale = tempLocale;
|
||||
} else {
|
||||
System.err.println("Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK.");
|
||||
}
|
||||
}
|
||||
|
||||
slr.setDefaultLocale(defaultLocale);
|
||||
return slr;
|
||||
}
|
||||
|
||||
}
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.LocaleResolver;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
|
||||
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
|
||||
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
|
||||
@Configuration
|
||||
public class Beans implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
ApplicationProperties applicationProperties;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(localeChangeInterceptor());
|
||||
registry.addInterceptor(new CleanUrlInterceptor());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LocaleChangeInterceptor localeChangeInterceptor() {
|
||||
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
|
||||
lci.setParamName("lang");
|
||||
return lci;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LocaleResolver localeResolver() {
|
||||
SessionLocaleResolver slr = new SessionLocaleResolver();
|
||||
|
||||
|
||||
String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale();
|
||||
Locale defaultLocale = Locale.UK; // Fallback to UK locale if environment variable is not set
|
||||
|
||||
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
|
||||
Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
|
||||
String tempLanguageTag = tempLocale.toLanguageTag();
|
||||
|
||||
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
|
||||
defaultLocale = tempLocale;
|
||||
} else {
|
||||
tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_","-"));
|
||||
tempLanguageTag = tempLocale.toLanguageTag();
|
||||
|
||||
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
|
||||
defaultLocale = tempLocale;
|
||||
} else {
|
||||
System.err.println("Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slr.setDefaultLocale(defaultLocale);
|
||||
return slr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,45 +1,70 @@
|
||||
package stirling.software.SPDF.config;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
public class CleanUrlInterceptor implements HandlerInterceptor {
|
||||
|
||||
private static final Pattern LANG_PATTERN = Pattern.compile("&?lang=([^&]+)");
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
String queryString = request.getQueryString();
|
||||
if (queryString != null && !queryString.isEmpty()) {
|
||||
String requestURI = request.getRequestURI();
|
||||
|
||||
// Keep the lang parameter if it exists
|
||||
Matcher langMatcher = LANG_PATTERN.matcher(queryString);
|
||||
String langQueryString = langMatcher.find() ? "lang=" + langMatcher.group(1) : "";
|
||||
|
||||
// Check if there are any other query parameters besides the lang parameter
|
||||
String remainingQueryString = queryString.replaceAll(LANG_PATTERN.pattern(), "").replaceAll("&+", "&").replaceAll("^&|&$", "");
|
||||
|
||||
if (!remainingQueryString.isEmpty()) {
|
||||
// Redirect to the URL without other query parameters
|
||||
String redirectUrl = requestURI + (langQueryString.isEmpty() ? "" : "?" + langQueryString);
|
||||
response.sendRedirect(redirectUrl);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
|
||||
}
|
||||
}
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
public class CleanUrlInterceptor implements HandlerInterceptor {
|
||||
|
||||
private static final List<String> ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error", "file");
|
||||
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||
throws Exception {
|
||||
String queryString = request.getQueryString();
|
||||
if (queryString != null && !queryString.isEmpty()) {
|
||||
String requestURI = request.getRequestURI();
|
||||
|
||||
Map<String, String> parameters = new HashMap<>();
|
||||
|
||||
// Keep only the allowed parameters
|
||||
String[] queryParameters = queryString.split("&");
|
||||
for (String param : queryParameters) {
|
||||
String[] keyValue = param.split("=");
|
||||
if (keyValue.length != 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ALLOWED_PARAMS.contains(keyValue[0])) {
|
||||
parameters.put(keyValue[0], keyValue[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// If there are any parameters that are not allowed
|
||||
if (parameters.size() != queryParameters.length) {
|
||||
// Construct new query string
|
||||
StringBuilder newQueryString = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : parameters.entrySet()) {
|
||||
if (newQueryString.length() > 0) {
|
||||
newQueryString.append("&");
|
||||
}
|
||||
newQueryString.append(entry.getKey()).append("=").append(entry.getValue());
|
||||
}
|
||||
|
||||
// Redirect to the URL with only allowed query parameters
|
||||
String redirectUrl = requestURI + "?" + newQueryString;
|
||||
response.sendRedirect(redirectUrl);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
|
||||
ModelAndView modelAndView) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
|
||||
Exception ex) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package stirling.software.SPDF.config;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import org.springframework.context.ApplicationContextInitializer;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
public class ConfigInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
||||
|
||||
@Override
|
||||
public void initialize(ConfigurableApplicationContext applicationContext) {
|
||||
try {
|
||||
ensureConfigExists();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to initialize application configuration", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void ensureConfigExists() throws IOException {
|
||||
// Define the path to the external config directory
|
||||
Path destPath = Paths.get("configs", "settings.yml");
|
||||
|
||||
// Check if the file already exists
|
||||
if (Files.notExists(destPath)) {
|
||||
// Ensure the destination directory exists
|
||||
Files.createDirectories(destPath.getParent());
|
||||
|
||||
// Copy the resource from classpath to the external directory
|
||||
try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
||||
if (in != null) {
|
||||
Files.copy(in, destPath);
|
||||
} else {
|
||||
throw new FileNotFoundException("Resource file not found: settings.yml.template");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,200 +1,228 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
@Service
|
||||
public class EndpointConfiguration {
|
||||
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
|
||||
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
||||
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
||||
|
||||
public EndpointConfiguration() {
|
||||
init();
|
||||
processEnvironmentConfigs();
|
||||
}
|
||||
|
||||
public void enableEndpoint(String endpoint) {
|
||||
endpointStatuses.put(endpoint, true);
|
||||
}
|
||||
|
||||
public void disableEndpoint(String endpoint) {
|
||||
if(!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
|
||||
logger.info("Disabling {}", endpoint);
|
||||
endpointStatuses.put(endpoint, false);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEndpointEnabled(String endpoint) {
|
||||
if (endpoint.startsWith("/")) {
|
||||
endpoint = endpoint.substring(1);
|
||||
}
|
||||
return endpointStatuses.getOrDefault(endpoint, true);
|
||||
}
|
||||
|
||||
public void addEndpointToGroup(String group, String endpoint) {
|
||||
endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint);
|
||||
}
|
||||
|
||||
public void enableGroup(String group) {
|
||||
Set<String> endpoints = endpointGroups.get(group);
|
||||
if (endpoints != null) {
|
||||
for (String endpoint : endpoints) {
|
||||
enableEndpoint(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void disableGroup(String group) {
|
||||
Set<String> endpoints = endpointGroups.get(group);
|
||||
if (endpoints != null) {
|
||||
for (String endpoint : endpoints) {
|
||||
disableEndpoint(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void init() {
|
||||
// Adding endpoints to "PageOps" group
|
||||
addEndpointToGroup("PageOps", "remove-pages");
|
||||
addEndpointToGroup("PageOps", "merge-pdfs");
|
||||
addEndpointToGroup("PageOps", "split-pdfs");
|
||||
addEndpointToGroup("PageOps", "pdf-organizer");
|
||||
addEndpointToGroup("PageOps", "rotate-pdf");
|
||||
addEndpointToGroup("PageOps", "multi-page-layout");
|
||||
addEndpointToGroup("PageOps", "scale-pages");
|
||||
|
||||
// Adding endpoints to "Convert" group
|
||||
addEndpointToGroup("Convert", "pdf-to-img");
|
||||
addEndpointToGroup("Convert", "img-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-pdfa");
|
||||
addEndpointToGroup("Convert", "file-to-pdf");
|
||||
addEndpointToGroup("Convert", "xlsx-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-word");
|
||||
addEndpointToGroup("Convert", "pdf-to-presentation");
|
||||
addEndpointToGroup("Convert", "pdf-to-text");
|
||||
addEndpointToGroup("Convert", "pdf-to-html");
|
||||
addEndpointToGroup("Convert", "pdf-to-xml");
|
||||
|
||||
// Adding endpoints to "Security" group
|
||||
addEndpointToGroup("Security", "add-password");
|
||||
addEndpointToGroup("Security", "remove-password");
|
||||
addEndpointToGroup("Security", "change-permissions");
|
||||
addEndpointToGroup("Security", "add-watermark");
|
||||
addEndpointToGroup("Security", "cert-sign");
|
||||
|
||||
|
||||
|
||||
// Adding endpoints to "Other" group
|
||||
addEndpointToGroup("Other", "ocr-pdf");
|
||||
addEndpointToGroup("Other", "add-image");
|
||||
addEndpointToGroup("Other", "compress-pdf");
|
||||
addEndpointToGroup("Other", "extract-images");
|
||||
addEndpointToGroup("Other", "change-metadata");
|
||||
addEndpointToGroup("Other", "extract-image-scans");
|
||||
addEndpointToGroup("Other", "sign");
|
||||
addEndpointToGroup("Other", "flatten");
|
||||
addEndpointToGroup("Other", "repair");
|
||||
addEndpointToGroup("Other", "remove-blanks");
|
||||
addEndpointToGroup("Other", "compare");
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//CLI
|
||||
addEndpointToGroup("CLI", "compress-pdf");
|
||||
addEndpointToGroup("CLI", "extract-image-scans");
|
||||
addEndpointToGroup("CLI", "remove-blanks");
|
||||
addEndpointToGroup("CLI", "repair");
|
||||
addEndpointToGroup("CLI", "pdf-to-pdfa");
|
||||
addEndpointToGroup("CLI", "file-to-pdf");
|
||||
addEndpointToGroup("CLI", "xlsx-to-pdf");
|
||||
addEndpointToGroup("CLI", "pdf-to-word");
|
||||
addEndpointToGroup("CLI", "pdf-to-presentation");
|
||||
addEndpointToGroup("CLI", "pdf-to-text");
|
||||
addEndpointToGroup("CLI", "pdf-to-html");
|
||||
addEndpointToGroup("CLI", "pdf-to-xml");
|
||||
addEndpointToGroup("CLI", "ocr-pdf");
|
||||
|
||||
//python
|
||||
addEndpointToGroup("Python", "extract-image-scans");
|
||||
addEndpointToGroup("Python", "remove-blanks");
|
||||
|
||||
|
||||
|
||||
//openCV
|
||||
addEndpointToGroup("OpenCV", "extract-image-scans");
|
||||
addEndpointToGroup("OpenCV", "remove-blanks");
|
||||
|
||||
//LibreOffice
|
||||
addEndpointToGroup("LibreOffice", "repair");
|
||||
addEndpointToGroup("LibreOffice", "file-to-pdf");
|
||||
addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-word");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-presentation");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-text");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-html");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-xml");
|
||||
|
||||
|
||||
//OCRmyPDF
|
||||
addEndpointToGroup("OCRmyPDF", "compress-pdf");
|
||||
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
|
||||
addEndpointToGroup("OCRmyPDF", "ocr-pdf");
|
||||
|
||||
//Java
|
||||
addEndpointToGroup("Java", "merge-pdfs");
|
||||
addEndpointToGroup("Java", "remove-pages");
|
||||
addEndpointToGroup("Java", "split-pdfs");
|
||||
addEndpointToGroup("Java", "pdf-organizer");
|
||||
addEndpointToGroup("Java", "rotate-pdf");
|
||||
addEndpointToGroup("Java", "pdf-to-img");
|
||||
addEndpointToGroup("Java", "img-to-pdf");
|
||||
addEndpointToGroup("Java", "add-password");
|
||||
addEndpointToGroup("Java", "remove-password");
|
||||
addEndpointToGroup("Java", "change-permissions");
|
||||
addEndpointToGroup("Java", "add-watermark");
|
||||
addEndpointToGroup("Java", "add-image");
|
||||
addEndpointToGroup("Java", "extract-images");
|
||||
addEndpointToGroup("Java", "change-metadata");
|
||||
addEndpointToGroup("Java", "cert-sign");
|
||||
addEndpointToGroup("Java", "multi-page-layout");
|
||||
addEndpointToGroup("Java", "scale-pages");
|
||||
|
||||
|
||||
//Javascript
|
||||
addEndpointToGroup("Javascript", "pdf-organizer");
|
||||
addEndpointToGroup("Javascript", "sign");
|
||||
addEndpointToGroup("Javascript", "compare");
|
||||
|
||||
}
|
||||
|
||||
private void processEnvironmentConfigs() {
|
||||
String endpointsToRemove = System.getenv("ENDPOINTS_TO_REMOVE");
|
||||
String groupsToRemove = System.getenv("GROUPS_TO_REMOVE");
|
||||
|
||||
if (endpointsToRemove != null) {
|
||||
String[] endpoints = endpointsToRemove.split(",");
|
||||
for (String endpoint : endpoints) {
|
||||
disableEndpoint(endpoint.trim());
|
||||
}
|
||||
}
|
||||
|
||||
if (groupsToRemove != null) {
|
||||
String[] groups = groupsToRemove.split(",");
|
||||
for (String group : groups) {
|
||||
disableGroup(group.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
@Service
|
||||
public class EndpointConfiguration {
|
||||
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
|
||||
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
||||
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@Autowired
|
||||
public EndpointConfiguration(ApplicationProperties applicationProperties) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
init();
|
||||
processEnvironmentConfigs();
|
||||
}
|
||||
|
||||
public void enableEndpoint(String endpoint) {
|
||||
endpointStatuses.put(endpoint, true);
|
||||
}
|
||||
|
||||
public void disableEndpoint(String endpoint) {
|
||||
if(!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
|
||||
logger.info("Disabling {}", endpoint);
|
||||
endpointStatuses.put(endpoint, false);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEndpointEnabled(String endpoint) {
|
||||
if (endpoint.startsWith("/")) {
|
||||
endpoint = endpoint.substring(1);
|
||||
}
|
||||
return endpointStatuses.getOrDefault(endpoint, true);
|
||||
}
|
||||
|
||||
public void addEndpointToGroup(String group, String endpoint) {
|
||||
endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint);
|
||||
}
|
||||
|
||||
public void enableGroup(String group) {
|
||||
Set<String> endpoints = endpointGroups.get(group);
|
||||
if (endpoints != null) {
|
||||
for (String endpoint : endpoints) {
|
||||
enableEndpoint(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void disableGroup(String group) {
|
||||
Set<String> endpoints = endpointGroups.get(group);
|
||||
if (endpoints != null) {
|
||||
for (String endpoint : endpoints) {
|
||||
disableEndpoint(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void init() {
|
||||
// Adding endpoints to "PageOps" group
|
||||
addEndpointToGroup("PageOps", "remove-pages");
|
||||
addEndpointToGroup("PageOps", "merge-pdfs");
|
||||
addEndpointToGroup("PageOps", "split-pdfs");
|
||||
addEndpointToGroup("PageOps", "pdf-organizer");
|
||||
addEndpointToGroup("PageOps", "rotate-pdf");
|
||||
addEndpointToGroup("PageOps", "multi-page-layout");
|
||||
addEndpointToGroup("PageOps", "scale-pages");
|
||||
addEndpointToGroup("PageOps", "adjust-contrast");
|
||||
addEndpointToGroup("PageOps", "crop");
|
||||
addEndpointToGroup("PageOps", "auto-split-pdf");
|
||||
addEndpointToGroup("PageOps", "extract-page");
|
||||
addEndpointToGroup("PageOps", "pdf-to-single-page");
|
||||
|
||||
// Adding endpoints to "Convert" group
|
||||
addEndpointToGroup("Convert", "pdf-to-img");
|
||||
addEndpointToGroup("Convert", "img-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-pdfa");
|
||||
addEndpointToGroup("Convert", "file-to-pdf");
|
||||
addEndpointToGroup("Convert", "xlsx-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-word");
|
||||
addEndpointToGroup("Convert", "pdf-to-presentation");
|
||||
addEndpointToGroup("Convert", "pdf-to-text");
|
||||
addEndpointToGroup("Convert", "pdf-to-html");
|
||||
addEndpointToGroup("Convert", "pdf-to-xml");
|
||||
addEndpointToGroup("Convert", "html-to-pdf");
|
||||
addEndpointToGroup("Convert", "url-to-pdf");
|
||||
addEndpointToGroup("Convert", "markdown-to-pdf");
|
||||
|
||||
// Adding endpoints to "Security" group
|
||||
addEndpointToGroup("Security", "add-password");
|
||||
addEndpointToGroup("Security", "remove-password");
|
||||
addEndpointToGroup("Security", "change-permissions");
|
||||
addEndpointToGroup("Security", "add-watermark");
|
||||
addEndpointToGroup("Security", "cert-sign");
|
||||
addEndpointToGroup("Security", "sanitize-pdf");
|
||||
|
||||
|
||||
// Adding endpoints to "Other" group
|
||||
addEndpointToGroup("Other", "ocr-pdf");
|
||||
addEndpointToGroup("Other", "add-image");
|
||||
addEndpointToGroup("Other", "compress-pdf");
|
||||
addEndpointToGroup("Other", "extract-images");
|
||||
addEndpointToGroup("Other", "change-metadata");
|
||||
addEndpointToGroup("Other", "extract-image-scans");
|
||||
addEndpointToGroup("Other", "sign");
|
||||
addEndpointToGroup("Other", "flatten");
|
||||
addEndpointToGroup("Other", "repair");
|
||||
addEndpointToGroup("Other", "remove-blanks");
|
||||
addEndpointToGroup("Other", "compare");
|
||||
addEndpointToGroup("Other", "add-page-numbers");
|
||||
addEndpointToGroup("Other", "auto-rename");
|
||||
addEndpointToGroup("Other", "get-info-on-pdf");
|
||||
addEndpointToGroup("Other", "show-javascript");
|
||||
|
||||
|
||||
|
||||
//CLI
|
||||
addEndpointToGroup("CLI", "compress-pdf");
|
||||
addEndpointToGroup("CLI", "extract-image-scans");
|
||||
addEndpointToGroup("CLI", "remove-blanks");
|
||||
addEndpointToGroup("CLI", "repair");
|
||||
addEndpointToGroup("CLI", "pdf-to-pdfa");
|
||||
addEndpointToGroup("CLI", "file-to-pdf");
|
||||
addEndpointToGroup("CLI", "xlsx-to-pdf");
|
||||
addEndpointToGroup("CLI", "pdf-to-word");
|
||||
addEndpointToGroup("CLI", "pdf-to-presentation");
|
||||
addEndpointToGroup("CLI", "pdf-to-text");
|
||||
addEndpointToGroup("CLI", "pdf-to-html");
|
||||
addEndpointToGroup("CLI", "pdf-to-xml");
|
||||
addEndpointToGroup("CLI", "ocr-pdf");
|
||||
addEndpointToGroup("CLI", "html-to-pdf");
|
||||
addEndpointToGroup("CLI", "url-to-pdf");
|
||||
|
||||
|
||||
//python
|
||||
addEndpointToGroup("Python", "extract-image-scans");
|
||||
addEndpointToGroup("Python", "remove-blanks");
|
||||
addEndpointToGroup("Python", "html-to-pdf");
|
||||
addEndpointToGroup("Python", "url-to-pdf");
|
||||
|
||||
//openCV
|
||||
addEndpointToGroup("OpenCV", "extract-image-scans");
|
||||
addEndpointToGroup("OpenCV", "remove-blanks");
|
||||
|
||||
//LibreOffice
|
||||
addEndpointToGroup("LibreOffice", "repair");
|
||||
addEndpointToGroup("LibreOffice", "file-to-pdf");
|
||||
addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-word");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-presentation");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-text");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-html");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-xml");
|
||||
|
||||
|
||||
//OCRmyPDF
|
||||
addEndpointToGroup("OCRmyPDF", "compress-pdf");
|
||||
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
|
||||
addEndpointToGroup("OCRmyPDF", "ocr-pdf");
|
||||
|
||||
//Java
|
||||
addEndpointToGroup("Java", "merge-pdfs");
|
||||
addEndpointToGroup("Java", "remove-pages");
|
||||
addEndpointToGroup("Java", "split-pdfs");
|
||||
addEndpointToGroup("Java", "pdf-organizer");
|
||||
addEndpointToGroup("Java", "rotate-pdf");
|
||||
addEndpointToGroup("Java", "pdf-to-img");
|
||||
addEndpointToGroup("Java", "img-to-pdf");
|
||||
addEndpointToGroup("Java", "add-password");
|
||||
addEndpointToGroup("Java", "remove-password");
|
||||
addEndpointToGroup("Java", "change-permissions");
|
||||
addEndpointToGroup("Java", "add-watermark");
|
||||
addEndpointToGroup("Java", "add-image");
|
||||
addEndpointToGroup("Java", "extract-images");
|
||||
addEndpointToGroup("Java", "change-metadata");
|
||||
addEndpointToGroup("Java", "cert-sign");
|
||||
addEndpointToGroup("Java", "multi-page-layout");
|
||||
addEndpointToGroup("Java", "scale-pages");
|
||||
addEndpointToGroup("Java", "add-page-numbers");
|
||||
addEndpointToGroup("Java", "auto-rename");
|
||||
addEndpointToGroup("Java", "auto-split-pdf");
|
||||
addEndpointToGroup("Java", "sanitize-pdf");
|
||||
addEndpointToGroup("Java", "crop");
|
||||
addEndpointToGroup("Java", "get-info-on-pdf");
|
||||
addEndpointToGroup("Java", "extract-page");
|
||||
addEndpointToGroup("Java", "pdf-to-single-page");
|
||||
addEndpointToGroup("Java", "markdown-to-pdf");
|
||||
addEndpointToGroup("Java", "show-javascript");
|
||||
|
||||
//Javascript
|
||||
addEndpointToGroup("Javascript", "pdf-organizer");
|
||||
addEndpointToGroup("Javascript", "sign");
|
||||
addEndpointToGroup("Javascript", "compare");
|
||||
addEndpointToGroup("Javascript", "adjust-contrast");
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void processEnvironmentConfigs() {
|
||||
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
|
||||
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
|
||||
|
||||
if (endpointsToRemove != null) {
|
||||
for (String endpoint : endpointsToRemove) {
|
||||
disableEndpoint(endpoint.trim());
|
||||
}
|
||||
}
|
||||
|
||||
if (groupsToRemove != null) {
|
||||
for (String group : groupsToRemove) {
|
||||
disableGroup(group.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
@Component
|
||||
public class EndpointInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Autowired
|
||||
private EndpointConfiguration endpointConfiguration;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||
throws Exception {
|
||||
String requestURI = request.getRequestURI();
|
||||
if (!endpointConfiguration.isEndpointEnabled(requestURI)) {
|
||||
response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
@Component
|
||||
public class EndpointInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Autowired
|
||||
private EndpointConfiguration endpointConfiguration;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||
throws Exception {
|
||||
String requestURI = request.getRequestURI();
|
||||
if (!endpointConfiguration.isEndpointEnabled(requestURI)) {
|
||||
response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
package stirling.software.SPDF.config;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import io.micrometer.core.instrument.Meter;
|
||||
import io.micrometer.core.instrument.config.MeterFilter;
|
||||
import io.micrometer.core.instrument.config.MeterFilterReply;
|
||||
|
||||
@Configuration
|
||||
public class MetricsConfig {
|
||||
|
||||
@Bean
|
||||
public MeterFilter meterFilter() {
|
||||
return new MeterFilter() {
|
||||
@Override
|
||||
public MeterFilterReply accept(Meter.Id id) {
|
||||
if (id.getName().equals("http.requests")) {
|
||||
return MeterFilterReply.NEUTRAL;
|
||||
}
|
||||
return MeterFilterReply.DENY;
|
||||
}
|
||||
};
|
||||
}
|
||||
package stirling.software.SPDF.config;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import io.micrometer.core.instrument.Meter;
|
||||
import io.micrometer.core.instrument.config.MeterFilter;
|
||||
import io.micrometer.core.instrument.config.MeterFilterReply;
|
||||
|
||||
@Configuration
|
||||
public class MetricsConfig {
|
||||
|
||||
@Bean
|
||||
public MeterFilter meterFilter() {
|
||||
return new MeterFilter() {
|
||||
@Override
|
||||
public MeterFilterReply accept(Meter.Id id) {
|
||||
if (id.getName().equals("http.requests")) {
|
||||
return MeterFilterReply.NEUTRAL;
|
||||
}
|
||||
return MeterFilterReply.DENY;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,48 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
@Component
|
||||
public class MetricsFilter extends OncePerRequestFilter {
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
@Autowired
|
||||
public MetricsFilter(MeterRegistry meterRegistry) {
|
||||
this.meterRegistry = meterRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
String uri = request.getRequestURI();
|
||||
|
||||
// Ignore static resources
|
||||
if (!(uri.startsWith("/js") || uri.startsWith("/images") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".svg")|| uri.endsWith(".js") || uri.contains("swagger") || uri.startsWith("/api"))) {
|
||||
Counter counter = Counter.builder("http.requests")
|
||||
.tag("uri", uri)
|
||||
.tag("method", request.getMethod())
|
||||
.register(meterRegistry);
|
||||
|
||||
counter.increment();
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
@Component
|
||||
public class MetricsFilter extends OncePerRequestFilter {
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
@Autowired
|
||||
public MetricsFilter(MeterRegistry meterRegistry) {
|
||||
this.meterRegistry = meterRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
String uri = request.getRequestURI();
|
||||
|
||||
//System.out.println("uri="+uri + ", method=" + request.getMethod() );
|
||||
// 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"))) {
|
||||
Counter counter = Counter.builder("http.requests")
|
||||
.tag("uri", uri)
|
||||
.tag("method", request.getMethod())
|
||||
.register(meterRegistry);
|
||||
|
||||
counter.increment();
|
||||
//System.out.println("Counted");
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
|
||||
@Configuration
|
||||
public class OpenApiConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
String version = getClass().getPackage().getImplementationVersion();
|
||||
if (version == null) {
|
||||
Properties props = new Properties();
|
||||
try (InputStream input = getClass().getClassLoader().getResourceAsStream("version.properties")) {
|
||||
props.load(input);
|
||||
version = props.getProperty("version");
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
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."));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
|
||||
@Configuration
|
||||
public class OpenApiConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
String version = getClass().getPackage().getImplementationVersion();
|
||||
if (version == null) {
|
||||
Properties props = new Properties();
|
||||
try (InputStream input = getClass().getClassLoader().getResourceAsStream("version.properties")) {
|
||||
props.load(input);
|
||||
version = props.getProperty("version");
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
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."));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class StartupApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
|
||||
|
||||
public static LocalDateTime startTime;
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
startTime = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private EndpointInterceptor endpointInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(endpointInterceptor);
|
||||
}
|
||||
}
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private EndpointInterceptor endpointInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(endpointInterceptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// Handler for external static resources
|
||||
registry.addResourceHandler("/**")
|
||||
.addResourceLocations("file:customFiles/static/", "classpath:/static/");
|
||||
//.setCachePeriod(0); // Optional: disable caching
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
|
||||
import org.springframework.core.env.PropertiesPropertySource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.io.support.EncodedResource;
|
||||
import org.springframework.core.io.support.PropertySourceFactory;
|
||||
public class YamlPropertySourceFactory implements PropertySourceFactory {
|
||||
|
||||
@Override
|
||||
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource)
|
||||
throws IOException {
|
||||
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
|
||||
factory.setResources(encodedResource.getResource());
|
||||
|
||||
Properties properties = factory.getObject();
|
||||
|
||||
return new PropertiesPropertySource(encodedResource.getResource().getFilename(), properties);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package stirling.software.SPDF.config.security;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
|
||||
throws IOException, ServletException {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package stirling.software.SPDF.config.security;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import stirling.software.SPDF.model.Authority;
|
||||
import stirling.software.SPDF.model.User;
|
||||
import stirling.software.SPDF.repository.UserRepository;
|
||||
|
||||
@Service
|
||||
public class CustomUserDetailsService implements UserDetailsService {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + username));
|
||||
|
||||
return new org.springframework.security.core.userdetails.User(
|
||||
user.getUsername(),
|
||||
user.getPassword(),
|
||||
user.isEnabled(),
|
||||
true, true, true,
|
||||
getAuthorities(user.getAuthorities())
|
||||
);
|
||||
}
|
||||
|
||||
private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) {
|
||||
return authorities.stream()
|
||||
.map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package stirling.software.SPDF.config.security;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import stirling.software.SPDF.config.security.UserService;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.Role;
|
||||
|
||||
@Component
|
||||
public class InitialSecuritySetup {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
ApplicationProperties applicationProperties;
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveKeyToConfig(String key) throws IOException {
|
||||
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
||||
List<String> lines = Files.readAllLines(path);
|
||||
boolean keyFound = false;
|
||||
|
||||
// Search for the existing key to replace it or place to add it
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
if (lines.get(i).startsWith("AutomaticallyGenerated:")) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the section doesn't exist, append it
|
||||
if (!keyFound) {
|
||||
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,103 @@
|
||||
package stirling.software.SPDF.config.security;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
|
||||
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
||||
@Configuration
|
||||
@EnableWebSecurity()
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||
public class SecurityConfiguration {
|
||||
|
||||
@Autowired
|
||||
private UserDetailsService userDetailsService;
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
@Autowired
|
||||
@Lazy
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("loginEnabled")
|
||||
public boolean loginEnabledValue;
|
||||
|
||||
@Autowired
|
||||
private UserAuthenticationFilter userAuthenticationFilter;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
if(loginEnabledValue) {
|
||||
|
||||
http.csrf(csrf -> csrf.disable());
|
||||
http
|
||||
.formLogin(formLogin -> formLogin
|
||||
.loginPage("/login")
|
||||
.defaultSuccessUrl("/")
|
||||
.failureHandler(new CustomAuthenticationFailureHandler())
|
||||
.permitAll()
|
||||
)
|
||||
.logout(logout -> logout
|
||||
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
|
||||
.logoutSuccessUrl("/login?logout=true")
|
||||
.invalidateHttpSession(true) // Invalidate session
|
||||
.deleteCookies("JSESSIONID", "remember-me")
|
||||
).rememberMe(rememberMeConfigurer -> rememberMeConfigurer // Use the configurator directly
|
||||
.key("uniqueAndSecret")
|
||||
.tokenRepository(persistentTokenRepository())
|
||||
.tokenValiditySeconds(1209600) // 2 weeks
|
||||
)
|
||||
.authorizeHttpRequests(authz -> authz
|
||||
.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/"))
|
||||
.permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.userDetailsService(userDetailsService)
|
||||
.authenticationProvider(authenticationProvider());
|
||||
} else {
|
||||
http.csrf(csrf -> csrf.disable())
|
||||
.authorizeHttpRequests(authz -> authz
|
||||
.anyRequest().permitAll()
|
||||
);
|
||||
}
|
||||
return http.build();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Bean
|
||||
public DaoAuthenticationProvider authenticationProvider() {
|
||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
||||
authProvider.setUserDetailsService(userDetailsService);
|
||||
authProvider.setPasswordEncoder(passwordEncoder());
|
||||
return authProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PersistentTokenRepository persistentTokenRepository() {
|
||||
return new JPATokenRepositoryImpl();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package stirling.software.SPDF.config.security;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
|
||||
@Component
|
||||
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
@Autowired
|
||||
private UserDetailsService userDetailsService;
|
||||
|
||||
@Autowired
|
||||
@Lazy
|
||||
private UserService userService;
|
||||
|
||||
|
||||
@Autowired
|
||||
@Qualifier("loginEnabled")
|
||||
public boolean loginEnabledValue;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
|
||||
if (!loginEnabledValue) {
|
||||
// If login is not enabled, just pass all requests without authentication
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
// Check for API key in the request headers if no authentication exists
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
String apiKey = request.getHeader("X-API-Key");
|
||||
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
||||
try {
|
||||
// Use API key to authenticate. This requires you to have an authentication provider for API keys.
|
||||
UserDetails userDetails = userService.loadUserByApiKey(apiKey);
|
||||
if(userDetails == null)
|
||||
{
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
response.getWriter().write("Invalid API Key.");
|
||||
return;
|
||||
}
|
||||
authentication = new ApiKeyAuthenticationToken(userDetails, apiKey, userDetails.getAuthorities());
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
} catch (AuthenticationException e) {
|
||||
// If API key authentication fails, deny the request
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
response.getWriter().write("Invalid API Key.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we still don't have any authentication, deny the request
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
String method = request.getMethod();
|
||||
if ("GET".equalsIgnoreCase(method)) {
|
||||
response.sendRedirect("/login"); // redirect to the login page
|
||||
return;
|
||||
}
|
||||
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");
|
||||
return;
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
|
||||
String uri = request.getRequestURI();
|
||||
|
||||
String[] permitAllPatterns = {
|
||||
"/login",
|
||||
"/register",
|
||||
"/error",
|
||||
"/images/",
|
||||
"/public/",
|
||||
"/css/",
|
||||
"/js/"
|
||||
};
|
||||
|
||||
for (String pattern : permitAllPatterns) {
|
||||
if (uri.startsWith(pattern) || uri.endsWith(".svg")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package stirling.software.SPDF.config.security;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import io.github.bucket4j.Bandwidth;
|
||||
import io.github.bucket4j.Bucket;
|
||||
import io.github.bucket4j.ConsumptionProbe;
|
||||
import io.github.bucket4j.Refill;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import stirling.software.SPDF.model.Role;
|
||||
@Component
|
||||
public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
||||
|
||||
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>();
|
||||
private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
|
||||
|
||||
@Autowired
|
||||
private UserDetailsService userDetailsService;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("rateLimit")
|
||||
public boolean rateLimit;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
if (!rateLimit) {
|
||||
// If rateLimit is not enabled, just pass all requests without rate limiting
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
String method = request.getMethod();
|
||||
if (!"POST".equalsIgnoreCase(method)) {
|
||||
// If the request is not a POST, just pass it through without rate limiting
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
String identifier = null;
|
||||
|
||||
// Check for API key in the request headers
|
||||
String apiKey = request.getHeader("X-API-Key");
|
||||
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
||||
identifier = "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
|
||||
} else {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
||||
identifier = userDetails.getUsername();
|
||||
}
|
||||
}
|
||||
|
||||
// If neither API key nor an authenticated user is present, use IP address
|
||||
if (identifier == null) {
|
||||
identifier = request.getRemoteAddr();
|
||||
}
|
||||
|
||||
Role userRole = getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
|
||||
|
||||
if (request.getHeader("X-API-Key") != null) {
|
||||
// It's an API call
|
||||
processRequest(userRole.getApiCallsPerDay(), identifier, apiBuckets, request, response, filterChain);
|
||||
} else {
|
||||
// It's a Web UI call
|
||||
processRequest(userRole.getWebCallsPerDay(), identifier, webBuckets, request, response, filterChain);
|
||||
}
|
||||
}
|
||||
|
||||
private Role getRoleFromAuthentication(Authentication authentication) {
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
for (GrantedAuthority authority : authentication.getAuthorities()) {
|
||||
try {
|
||||
return Role.fromString(authority.getAuthority());
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// Ignore and continue to next authority.
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("User does not have a valid role.");
|
||||
}
|
||||
|
||||
private void processRequest(int limitPerDay, String identifier, Map<String, Bucket> buckets,
|
||||
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws IOException, ServletException {
|
||||
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay));
|
||||
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
|
||||
|
||||
if (probe.isConsumed()) {
|
||||
response.setHeader("X-Rate-Limit-Remaining", Long.toString(probe.getRemainingTokens()));
|
||||
filterChain.doFilter(request, response);
|
||||
} else {
|
||||
long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;
|
||||
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
|
||||
response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill));
|
||||
response.getWriter().write("Rate limit exceeded for POST requests.");
|
||||
}
|
||||
}
|
||||
|
||||
private Bucket createUserBucket(int limitPerDay) {
|
||||
Bandwidth limit = Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1)));
|
||||
return Bucket.builder().addLimit(limit).build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package stirling.software.SPDF.config.security;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import stirling.software.SPDF.model.Authority;
|
||||
import stirling.software.SPDF.model.User;
|
||||
import stirling.software.SPDF.repository.UserRepository;
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
public Authentication getAuthentication(String apiKey) {
|
||||
User user = getUserByApiKey(apiKey);
|
||||
if (user == null) {
|
||||
throw new UsernameNotFoundException("API key is not valid");
|
||||
}
|
||||
|
||||
// Convert the user into an Authentication object
|
||||
return new UsernamePasswordAuthenticationToken(
|
||||
user, // principal (typically the user)
|
||||
null, // credentials (we don't expose the password or API key here)
|
||||
getAuthorities(user) // user's authorities (roles/permissions)
|
||||
);
|
||||
}
|
||||
|
||||
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
|
||||
// Convert each Authority object into a SimpleGrantedAuthority object.
|
||||
return user.getAuthorities().stream()
|
||||
.map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
|
||||
}
|
||||
|
||||
private String generateApiKey() {
|
||||
String apiKey;
|
||||
do {
|
||||
apiKey = UUID.randomUUID().toString();
|
||||
} while (userRepository.findByApiKey(apiKey) != null); // Ensure uniqueness
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
public User addApiKeyToUser(String username) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||
|
||||
user.setApiKey(generateApiKey());
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
public User refreshApiKeyForUser(String username) {
|
||||
return addApiKeyToUser(username); // reuse the add API key method for refreshing
|
||||
}
|
||||
|
||||
public String getApiKeyForUser(String username) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||
return user.getApiKey();
|
||||
}
|
||||
|
||||
public boolean isValidApiKey(String apiKey) {
|
||||
return userRepository.findByApiKey(apiKey) != null;
|
||||
}
|
||||
|
||||
public User getUserByApiKey(String apiKey) {
|
||||
return userRepository.findByApiKey(apiKey);
|
||||
}
|
||||
|
||||
public UserDetails loadUserByApiKey(String apiKey) {
|
||||
User userOptional = userRepository.findByApiKey(apiKey);
|
||||
if (userOptional != null) {
|
||||
User user = userOptional;
|
||||
// Convert your User entity to a UserDetails object with authorities
|
||||
return new org.springframework.security.core.userdetails.User(
|
||||
user.getUsername(),
|
||||
user.getPassword(), // you might not need this for API key auth
|
||||
getAuthorities(user)
|
||||
);
|
||||
}
|
||||
return null; // or throw an exception
|
||||
}
|
||||
|
||||
|
||||
public boolean validateApiKeyForUser(String username, String apiKey) {
|
||||
Optional<User> userOpt = userRepository.findByUsername(username);
|
||||
return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey);
|
||||
}
|
||||
|
||||
public void saveUser(String username, String password) {
|
||||
User user = new User();
|
||||
user.setUsername(username);
|
||||
user.setPassword(passwordEncoder.encode(password));
|
||||
user.setEnabled(true);
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
public void saveUser(String username, String password, String role) {
|
||||
User user = new User();
|
||||
user.setUsername(username);
|
||||
user.setPassword(passwordEncoder.encode(password));
|
||||
user.addAuthority(new Authority(role, user));
|
||||
user.setEnabled(true);
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
public void deleteUser(String username) {
|
||||
Optional<User> userOpt = userRepository.findByUsername(username);
|
||||
if (userOpt.isPresent()) {
|
||||
userRepository.delete(userOpt.get());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean usernameExists(String username) {
|
||||
return userRepository.findByUsername(username).isPresent();
|
||||
}
|
||||
|
||||
public boolean hasUsers() {
|
||||
return userRepository.count() > 0;
|
||||
}
|
||||
|
||||
public void updateUserSettings(String username, Map<String, String> updates) {
|
||||
Optional<User> userOpt = userRepository.findByUsername(username);
|
||||
if (userOpt.isPresent()) {
|
||||
User user = userOpt.get();
|
||||
Map<String, String> settingsMap = user.getSettings();
|
||||
|
||||
if(settingsMap == null) {
|
||||
settingsMap = new HashMap<String,String>();
|
||||
}
|
||||
settingsMap.clear();
|
||||
settingsMap.putAll(updates);
|
||||
user.setSettings(settingsMap);
|
||||
|
||||
userRepository.save(user);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<User> findByUsername(String username) {
|
||||
return userRepository.findByUsername(username);
|
||||
}
|
||||
|
||||
public void changeUsername(User user, String newUsername) {
|
||||
user.setUsername(newUsername);
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
public void changePassword(User user, String newPassword) {
|
||||
user.setPassword(passwordEncoder.encode(newPassword));
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
public boolean isPasswordCorrect(User user, String currentPassword) {
|
||||
return passwordEncoder.matches(currentPassword, user.getPassword());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.itextpdf.kernel.geom.PageSize;
|
||||
import com.itextpdf.kernel.pdf.PdfDocument;
|
||||
import com.itextpdf.kernel.pdf.PdfPage;
|
||||
import com.itextpdf.kernel.pdf.PdfReader;
|
||||
import com.itextpdf.kernel.pdf.PdfWriter;
|
||||
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
|
||||
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class CropController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
|
||||
|
||||
|
||||
@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")
|
||||
public ResponseEntity<byte[]> cropPdf(
|
||||
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
|
||||
@Parameter(description = "The x-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("x") float x,
|
||||
@Parameter(description = "The y-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("y") float y,
|
||||
@Parameter(description = "The width of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("width") float width,
|
||||
@Parameter(description = "The height of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("height") float height) throws IOException {
|
||||
byte[] bytes = file.getBytes();
|
||||
System.out.println("x=" + x + ", " + "y=" + y + ", " + "width=" + width + ", " +"height=" + height );
|
||||
PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes));
|
||||
PdfDocument pdfDoc = new PdfDocument(reader);
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
PdfWriter writer = new PdfWriter(baos);
|
||||
PdfDocument outputPdf = new PdfDocument(writer);
|
||||
|
||||
int totalPages = pdfDoc.getNumberOfPages();
|
||||
|
||||
for (int i = 1; i <= totalPages; i++) {
|
||||
PdfPage page = outputPdf.addNewPage(new PageSize(width, height));
|
||||
PdfCanvas pdfCanvas = new PdfCanvas(page);
|
||||
|
||||
PdfFormXObject formXObject = pdfDoc.getPage(i).copyAsFormXObject(outputPdf);
|
||||
|
||||
// Save the graphics state, apply the transformations, add the object, and then
|
||||
// restore the graphics state
|
||||
pdfCanvas.saveState();
|
||||
pdfCanvas.rectangle(x, y, width, height);
|
||||
pdfCanvas.clip();
|
||||
pdfCanvas.addXObject(formXObject, -x, -y);
|
||||
pdfCanvas.restoreState();
|
||||
}
|
||||
|
||||
|
||||
outputPdf.close();
|
||||
byte[] pdfContent = baos.toByteArray();
|
||||
pdfDoc.close();
|
||||
return WebResponseUtils.bytesToWebResponse(pdfContent,
|
||||
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,78 +1,125 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageTree;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
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.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class MergeController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MergeController.class);
|
||||
|
||||
private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
|
||||
// Create a new empty document
|
||||
PDDocument mergedDoc = new PDDocument();
|
||||
|
||||
// Iterate over the list of documents and add their pages to the merged document
|
||||
for (PDDocument doc : documents) {
|
||||
// Get all pages from the current document
|
||||
PDPageTree pages = doc.getPages();
|
||||
// Iterate over the pages and add them to the merged document
|
||||
for (PDPage page : pages) {
|
||||
mergedDoc.addPage(page);
|
||||
}
|
||||
|
||||
|
||||
private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
|
||||
PDDocument mergedDoc = new PDDocument();
|
||||
for (PDDocument doc : documents) {
|
||||
for (PDPage page : doc.getPages()) {
|
||||
mergedDoc.addPage(page);
|
||||
}
|
||||
}
|
||||
return mergedDoc;
|
||||
}
|
||||
|
||||
// Return the merged document
|
||||
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(
|
||||
@RequestPart(required = true, value = "fileInput") MultipartFile[] files,
|
||||
@RequestParam(value = "sortType", defaultValue = "orderProvided")
|
||||
@Parameter(schema = @Schema(description = "The type of sorting to be applied on the input files before merging.",
|
||||
allowableValues = {
|
||||
"orderProvided",
|
||||
"byFileName",
|
||||
"byDateModified",
|
||||
"byDateCreated",
|
||||
"byPDFTitle"
|
||||
}))
|
||||
String sortType) throws IOException {
|
||||
|
||||
Arrays.sort(files, getSortComparator(sortType));
|
||||
|
||||
List<PDDocument> documents = new ArrayList<>();
|
||||
for (MultipartFile file : files) {
|
||||
try (InputStream is = file.getInputStream()) {
|
||||
documents.add(PDDocument.load(is));
|
||||
}
|
||||
}
|
||||
|
||||
@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."
|
||||
)
|
||||
public ResponseEntity<byte[]> mergePdfs(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@Parameter(description = "The input PDF files to be merged into a single file", required = true)
|
||||
MultipartFile[] files) throws IOException {
|
||||
// Read the input PDF files into PDDocument objects
|
||||
List<PDDocument> documents = new ArrayList<>();
|
||||
|
||||
// Loop through the files array and read each file into a PDDocument
|
||||
for (MultipartFile file : files) {
|
||||
documents.add(PDDocument.load(file.getInputStream()));
|
||||
}
|
||||
|
||||
PDDocument mergedDoc = mergeDocuments(documents);
|
||||
|
||||
|
||||
// Return the merged PDF as a response
|
||||
try (PDDocument mergedDoc = mergeDocuments(documents)) {
|
||||
ResponseEntity<byte[]> response = WebResponseUtils.pdfDocToWebResponse(mergedDoc, files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf");
|
||||
|
||||
for (PDDocument doc : documents) {
|
||||
// Close the document after processing
|
||||
doc.close();
|
||||
}
|
||||
|
||||
return response;
|
||||
} finally {
|
||||
for (PDDocument doc : documents) {
|
||||
if (doc != null) {
|
||||
doc.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,15 +24,17 @@ import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class MultiPageLayoutController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class);
|
||||
|
||||
@PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data")
|
||||
@Operation(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.")
|
||||
@Operation(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")
|
||||
public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(
|
||||
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
|
||||
@Parameter(description = "The number of pages to fit onto a single sheet in the output PDF. Acceptable values are 2, 3, 4, 9, 16.", required = true, schema = @Schema(type = "integer", allowableValues = {
|
||||
|
||||
@@ -18,16 +18,18 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class RearrangePagesPDFController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class);
|
||||
|
||||
@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.")
|
||||
@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")
|
||||
public ResponseEntity<byte[]> deletePages(
|
||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file from which pages will be removed") MultipartFile pdfFile,
|
||||
@RequestParam("pagesToDelete") @Parameter(description = "Comma-separated list of pages or page ranges to delete, e.g., '1,3,5-8'") String pagesToDelete)
|
||||
@@ -151,7 +153,7 @@ public class RearrangePagesPDFController {
|
||||
}
|
||||
|
||||
@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.")
|
||||
@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(
|
||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to rearrange pages") MultipartFile pdfFile,
|
||||
@RequestParam(required = false, value = "pageOrder") @Parameter(description = "The new page order as a comma-separated list of page numbers, page ranges (e.g., '1,3,5-7'), or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')") String pageOrder,
|
||||
|
||||
@@ -16,9 +16,11 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class RotationController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RotationController.class);
|
||||
@@ -26,7 +28,7 @@ public class RotationController {
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/rotate-pdf")
|
||||
@Operation(
|
||||
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."
|
||||
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(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
|
||||
@@ -39,22 +39,24 @@ import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class ScalePagesController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ScalePagesController.class);
|
||||
|
||||
@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.")
|
||||
@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")
|
||||
public ResponseEntity<byte[]> scalePages(
|
||||
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
|
||||
@Parameter(description = "The scale of pages in the output PDF. Acceptable values are A0-A10, B0-B9, LETTER, TABLOID, LEDGER, LEGAL, EXECUTIVE.", required = true, schema = @Schema(type = "String", allowableValues = {
|
||||
@Parameter(description = "The scale of pages in the output PDF. Acceptable values are A0-A10, B0-B9, LETTER, TABLOID, LEDGER, LEGAL, EXECUTIVE.", required = true, schema = @Schema(type = "string", allowableValues = {
|
||||
"A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10", "B0", "B1", "B2", "B3", "B4",
|
||||
"B5", "B6", "B7", "B8", "B9", "LETTER", "TABLOID", "LEDGER", "LEGAL",
|
||||
"EXECUTIVE" })) @RequestParam("pageSize") String targetPageSize,
|
||||
@Parameter(description = "The scale of the content on the pages of the output PDF. Acceptable values are floats.", required = true, schema = @Schema(type = "float")) @RequestParam("scaleFactor") float scaleFactor)
|
||||
@Parameter(description = "The scale of the content on the pages of the output PDF. Acceptable values are floats.", required = true, schema = @Schema(type = "integer")) @RequestParam("scaleFactor") float scaleFactor)
|
||||
throws IOException {
|
||||
|
||||
Map<String, PageSize> sizeMap = new HashMap<>();
|
||||
|
||||
@@ -15,9 +15,6 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -28,17 +25,19 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class SplitPDFController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SplitPDFController.class);
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/split-pages")
|
||||
@Operation(summary = "Split a PDF file into separate documents",
|
||||
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.")
|
||||
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")
|
||||
public ResponseEntity<byte[]> splitPdf(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@Parameter(description = "The input PDF file to be split")
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.itextpdf.kernel.geom.PageSize;
|
||||
import com.itextpdf.kernel.geom.Rectangle;
|
||||
import com.itextpdf.kernel.pdf.PdfDocument;
|
||||
import com.itextpdf.kernel.pdf.PdfReader;
|
||||
import com.itextpdf.kernel.pdf.PdfWriter;
|
||||
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
|
||||
import com.itextpdf.layout.Document;
|
||||
import com.itextpdf.layout.element.Image;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@RestController
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class ToSinglePageController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ToSinglePageController.class);
|
||||
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-single-page")
|
||||
@Operation(
|
||||
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"
|
||||
)
|
||||
public ResponseEntity<byte[]> pdfToSinglePage(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@Parameter(description = "The input multi-page PDF file to be converted into a single page", required = true)
|
||||
MultipartFile file) throws IOException {
|
||||
|
||||
PdfReader reader = new PdfReader(file.getInputStream());
|
||||
PdfDocument sourceDocument = new PdfDocument(reader);
|
||||
|
||||
float totalHeight = 0;
|
||||
float width = 0;
|
||||
|
||||
for (int i = 1; i <= sourceDocument.getNumberOfPages(); i++) {
|
||||
Rectangle pageSize = sourceDocument.getPage(i).getPageSize();
|
||||
totalHeight += pageSize.getHeight();
|
||||
if(width < pageSize.getWidth())
|
||||
width = pageSize.getWidth();
|
||||
}
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
PdfWriter writer = new PdfWriter(baos);
|
||||
PdfDocument newDocument = new PdfDocument(writer);
|
||||
PageSize newPageSize = new PageSize(width, totalHeight);
|
||||
newDocument.addNewPage(newPageSize);
|
||||
|
||||
Document layoutDoc = new Document(newDocument);
|
||||
float yOffset = totalHeight;
|
||||
|
||||
for (int i = 1; i <= sourceDocument.getNumberOfPages(); i++) {
|
||||
PdfFormXObject pageCopy = sourceDocument.getPage(i).copyAsFormXObject(newDocument);
|
||||
Image copiedPage = new Image(pageCopy);
|
||||
copiedPage.setFixedPosition(0, yOffset - sourceDocument.getPage(i).getPageSize().getHeight());
|
||||
yOffset -= sourceDocument.getPage(i).getPageSize().getHeight();
|
||||
layoutDoc.add(copiedPage);
|
||||
}
|
||||
|
||||
layoutDoc.close();
|
||||
sourceDocument.close();
|
||||
|
||||
byte[] result = baos.toByteArray();
|
||||
return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_singlePage.pdf");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import stirling.software.SPDF.config.security.UserService;
|
||||
import stirling.software.SPDF.model.User;
|
||||
|
||||
@Controller
|
||||
public class UserController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@PostMapping("/register")
|
||||
public String register(@RequestParam String username, @RequestParam String password, Model model) {
|
||||
if(userService.usernameExists(username)) {
|
||||
model.addAttribute("error", "Username already exists");
|
||||
return "register";
|
||||
}
|
||||
|
||||
userService.saveUser(username, password);
|
||||
return "redirect:/login?registered=true";
|
||||
}
|
||||
|
||||
@PostMapping("/change-username")
|
||||
public ResponseEntity<String> changeUsername(Principal principal, @RequestParam String currentPassword, @RequestParam String newUsername, HttpServletRequest request, HttpServletResponse response) {
|
||||
if (principal == null) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
|
||||
}
|
||||
|
||||
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
||||
|
||||
if(userOpt == null || userOpt.isEmpty()) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found.");
|
||||
}
|
||||
User user = userOpt.get();
|
||||
|
||||
if(!userService.isPasswordCorrect(user, currentPassword)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Current password is incorrect.");
|
||||
}
|
||||
|
||||
if(userService.usernameExists(newUsername)) {
|
||||
return ResponseEntity.status(HttpStatus.CONFLICT).body("New username already exists.");
|
||||
}
|
||||
|
||||
userService.changeUsername(user, newUsername);
|
||||
|
||||
// Logout using Spring's utility
|
||||
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||
|
||||
|
||||
return ResponseEntity.ok("Username updated successfully.");
|
||||
}
|
||||
|
||||
@PostMapping("/change-password")
|
||||
public ResponseEntity<String> changePassword(Principal principal, @RequestParam String currentPassword, @RequestParam String newPassword, HttpServletRequest request, HttpServletResponse response) {
|
||||
if (principal == null) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
|
||||
}
|
||||
|
||||
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
||||
|
||||
if(userOpt == null || userOpt.isEmpty()) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found.");
|
||||
}
|
||||
User user = userOpt.get();
|
||||
if(!userService.isPasswordCorrect(user, currentPassword)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Current password is incorrect.");
|
||||
}
|
||||
|
||||
userService.changePassword(user, newPassword);
|
||||
|
||||
// Logout using Spring's utility
|
||||
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||
|
||||
return ResponseEntity.ok("Password updated successfully.");
|
||||
}
|
||||
|
||||
@PostMapping("/updateUserSettings")
|
||||
public String updateUserSettings(HttpServletRequest request, Principal principal) {
|
||||
Map<String, String[]> paramMap = request.getParameterMap();
|
||||
Map<String, String> updates = new HashMap<>();
|
||||
|
||||
System.out.println("Received parameter map: " + paramMap);
|
||||
|
||||
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
|
||||
updates.put(entry.getKey(), entry.getValue()[0]);
|
||||
}
|
||||
|
||||
System.out.println("Processed updates: " + updates);
|
||||
|
||||
// Assuming you have a method in userService to update the settings for a user
|
||||
userService.updateUserSettings(principal.getName(), updates);
|
||||
|
||||
return "redirect:/account"; // Redirect to a page of your choice after updating
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@PostMapping("/admin/saveUser")
|
||||
public String saveUser(@RequestParam String username, @RequestParam String password, @RequestParam String role) {
|
||||
userService.saveUser(username, password, role);
|
||||
return "redirect:/addUsers"; // Redirect to account page after adding the user
|
||||
}
|
||||
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@GetMapping("/admin/deleteUser/{username}")
|
||||
public String deleteUser(@PathVariable String username) {
|
||||
userService.deleteUser(username);
|
||||
return "redirect:/addUsers";
|
||||
}
|
||||
|
||||
@PostMapping("/get-api-key")
|
||||
public ResponseEntity<String> getApiKey(Principal principal) {
|
||||
if (principal == null) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
|
||||
}
|
||||
String username = principal.getName();
|
||||
String apiKey = userService.getApiKeyForUser(username);
|
||||
if (apiKey == null) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user.");
|
||||
}
|
||||
return ResponseEntity.ok(apiKey);
|
||||
}
|
||||
|
||||
@PostMapping("/update-api-key")
|
||||
public ResponseEntity<String> updateApiKey(Principal principal) {
|
||||
if (principal == null) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated.");
|
||||
}
|
||||
String username = principal.getName();
|
||||
User user = userService.refreshApiKeyForUser(username);
|
||||
String apiKey = user.getApiKey();
|
||||
if (apiKey == null) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("API key not found for user.");
|
||||
}
|
||||
return ResponseEntity.ok(apiKey);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
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.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
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.utils.FileToPdf;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@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(
|
||||
@RequestPart(required = true, value = "fileInput") MultipartFile fileInput)
|
||||
throws Exception {
|
||||
|
||||
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 iText or 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package stirling.software.SPDF.controller.api.converters;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
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.utils.FileToPdf;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
public class ConvertHtmlToPDF {
|
||||
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/html-to-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(
|
||||
@RequestPart(required = true, value = "fileInput") MultipartFile fileInput) throws IOException, InterruptedException {
|
||||
|
||||
if (fileInput == null) {
|
||||
throw new IllegalArgumentException("Please provide an HTML or ZIP file for conversion.");
|
||||
}
|
||||
|
||||
String originalFilename = fileInput.getOriginalFilename();
|
||||
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 outputFilename = originalFilename.replaceFirst("[.][^.]+$", "") + ".pdf"; // Remove file extension and append .pdf
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -20,16 +20,18 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.PdfUtils;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@RestController
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
public class ConvertImgPDFController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class);
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-img")
|
||||
@Operation(summary = "Convert PDF to image(s)",
|
||||
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.")
|
||||
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(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@Parameter(description = "The input PDF file to be converted")
|
||||
@@ -41,7 +43,7 @@ public class ConvertImgPDFController {
|
||||
@Parameter(description = "Choose between a single image containing all pages or separate images for each page", schema = @Schema(allowableValues = {"single", "multiple"}))
|
||||
String singleOrMultiple,
|
||||
@RequestParam("colorType")
|
||||
@Parameter(description = "The color type of the output image(s)", schema = @Schema(allowableValues = {"rgb", "greyscale", "blackwhite"}))
|
||||
@Parameter(description = "The color type of the output image(s)", schema = @Schema(allowableValues = {"color", "greyscale", "blackwhite"}))
|
||||
String colorType,
|
||||
@RequestParam("dpi")
|
||||
@Parameter(description = "The DPI (dots per inch) for the output image(s)")
|
||||
@@ -83,7 +85,7 @@ public class ConvertImgPDFController {
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/img-to-pdf")
|
||||
@Operation(summary = "Convert images to a PDF file",
|
||||
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.")
|
||||
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(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@Parameter(description = "The input images to be converted to a PDF file")
|
||||
@@ -92,7 +94,7 @@ public class ConvertImgPDFController {
|
||||
@Parameter(description = "Whether to stretch the images to fit the PDF page or maintain the aspect ratio", example = "false")
|
||||
boolean stretchToFit,
|
||||
@RequestParam("colorType")
|
||||
@Parameter(description = "The color type of the output image(s)", schema = @Schema(allowableValues = {"rgb", "greyscale", "blackwhite"}))
|
||||
@Parameter(description = "The color type of the output image(s)", schema = @Schema(allowableValues = {"color", "greyscale", "blackwhite"}))
|
||||
String colorType,
|
||||
@RequestParam(defaultValue = "false", name = "autoRotate")
|
||||
@Parameter(description = "Whether to automatically rotate the images to better fit the PDF page", example = "true")
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package stirling.software.SPDF.controller.api.converters;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.parser.Parser;
|
||||
import org.commonmark.renderer.html.HtmlRenderer;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
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.utils.FileToPdf;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
public class ConvertMarkdownToPdf {
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/markdown-to-pdf")
|
||||
@Operation(
|
||||
summary = "Convert a Markdown file to PDF",
|
||||
description = "This endpoint takes a Markdown file input, converts it to HTML, and then to PDF format."
|
||||
)
|
||||
public ResponseEntity<byte[]> markdownToPdf(
|
||||
@RequestPart(required = true, value = "fileInput") MultipartFile fileInput)
|
||||
throws IOException, InterruptedException {
|
||||
|
||||
if (fileInput == null) {
|
||||
throw new IllegalArgumentException("Please provide a Markdown file for conversion.");
|
||||
}
|
||||
|
||||
String originalFilename = fileInput.getOriginalFilename();
|
||||
if (originalFilename == null || !originalFilename.endsWith(".md")) {
|
||||
throw new IllegalArgumentException("File must be in .md format.");
|
||||
}
|
||||
|
||||
// Convert Markdown to HTML using CommonMark
|
||||
Parser parser = Parser.builder().build();
|
||||
Node document = parser.parse(new String(fileInput.getBytes()));
|
||||
HtmlRenderer renderer = HtmlRenderer.builder().build();
|
||||
String htmlContent = renderer.render(document);
|
||||
|
||||
byte[] pdfBytes = FileToPdf.convertHtmlToPdf(htmlContent.getBytes(), "converted.html");
|
||||
|
||||
String outputFilename = originalFilename.replaceFirst("[.][^.]+$", "") + ".pdf"; // Remove file extension and append .pdf
|
||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,13 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
public class ConvertOfficeController {
|
||||
|
||||
public byte[] convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
|
||||
@@ -39,7 +42,7 @@ public class ConvertOfficeController {
|
||||
|
||||
// Run the LibreOffice command
|
||||
List<String> command = new ArrayList<>(Arrays.asList("unoconv", "-vvv", "-f", "pdf", "-o", tempOutputFile.toString(), tempInputFile.toString()));
|
||||
int returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE).runCommandWithOutputHandling(command);
|
||||
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE).runCommandWithOutputHandling(command);
|
||||
|
||||
// Read the converted PDF file
|
||||
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||
@@ -57,13 +60,13 @@ public class ConvertOfficeController {
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/file-to-pdf")
|
||||
@Operation(
|
||||
summary = "Convert a file to a PDF using OCR",
|
||||
description = "This endpoint converts a given file to a PDF using Optical Character Recognition (OCR). The filename of the resulting PDF will be the original filename with '_convertedToPDF.pdf' appended."
|
||||
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"
|
||||
)
|
||||
public ResponseEntity<byte[]> processPdfWithOCR(
|
||||
public ResponseEntity<byte[]> processFileToPDF(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@Parameter(
|
||||
description = "The input file to be converted to a PDF file using OCR",
|
||||
description = "The input file to be converted to a PDF file using LibreOffice",
|
||||
required = true
|
||||
)
|
||||
MultipartFile inputFile
|
||||
|
||||
@@ -12,13 +12,15 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.PDFToFile;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
public class ConvertPDFToOffice {
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-html")
|
||||
@Operation(summary = "Convert PDF to HTML", description = "This endpoint converts a PDF file to HTML format.")
|
||||
@Operation(summary = "Convert PDF to HTML", description = "This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO")
|
||||
public ResponseEntity<byte[]> processPdfToHTML(
|
||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to HTML format", required = true) MultipartFile inputFile)
|
||||
throws IOException, InterruptedException {
|
||||
@@ -27,7 +29,7 @@ public class ConvertPDFToOffice {
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-presentation")
|
||||
@Operation(summary = "Convert PDF to Presentation format", description = "This endpoint converts a given PDF file to a Presentation format.")
|
||||
@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")
|
||||
public ResponseEntity<byte[]> processPdfToPresentation(
|
||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile,
|
||||
@RequestParam("outputFormat") @Parameter(description = "The output Presentation format", schema = @Schema(allowableValues = {
|
||||
@@ -38,7 +40,7 @@ public class ConvertPDFToOffice {
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-text")
|
||||
@Operation(summary = "Convert PDF to Text or RTF format", description = "This endpoint converts a given PDF file to Text or RTF format.")
|
||||
@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")
|
||||
public ResponseEntity<byte[]> processPdfToRTForTXT(
|
||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile,
|
||||
@RequestParam("outputFormat") @Parameter(description = "The output Text or RTF format", schema = @Schema(allowableValues = {
|
||||
@@ -49,7 +51,7 @@ public class ConvertPDFToOffice {
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-word")
|
||||
@Operation(summary = "Convert PDF to Word document", description = "This endpoint converts a given PDF file to a Word document format.")
|
||||
@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")
|
||||
public ResponseEntity<byte[]> processPdfToWord(
|
||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile,
|
||||
@RequestParam("outputFormat") @Parameter(description = "The output Word document format", schema = @Schema(allowableValues = {
|
||||
@@ -60,7 +62,7 @@ public class ConvertPDFToOffice {
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-xml")
|
||||
@Operation(summary = "Convert PDF to XML", description = "This endpoint converts a PDF file to an XML file.")
|
||||
@Operation(summary = "Convert PDF to XML", description = "This endpoint converts a PDF file to an XML file. Input:PDF Output:XML Type:SISO")
|
||||
public ResponseEntity<byte[]> processPdfToXML(
|
||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to an XML file", required = true) MultipartFile inputFile)
|
||||
throws IOException, InterruptedException {
|
||||
|
||||
@@ -14,16 +14,19 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
public class ConvertPDFToPDFA {
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-pdfa")
|
||||
@Operation(
|
||||
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."
|
||||
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(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@@ -47,7 +50,7 @@ public class ConvertPDFToPDFA {
|
||||
command.add(tempInputFile.toString());
|
||||
command.add(tempOutputFile.toString());
|
||||
|
||||
int 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
|
||||
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package stirling.software.SPDF.controller.api.converters;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
public class ConvertWebsiteToPDF {
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/url-to-pdf")
|
||||
@Operation(
|
||||
summary = "Convert a URL to a PDF",
|
||||
description = "This endpoint fetches content from a URL and converts it to a PDF format."
|
||||
)
|
||||
public ResponseEntity<byte[]> urlToPdf(
|
||||
@RequestParam(required = true, value = "urlInput")
|
||||
@Parameter(description = "The input URL to be converted to a PDF file", required = true)
|
||||
String URL) throws IOException, InterruptedException {
|
||||
|
||||
// Validate the URL format
|
||||
if(!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
|
||||
throw new IllegalArgumentException("Invalid URL format provided.");
|
||||
}
|
||||
Path tempOutputFile = null;
|
||||
byte[] pdfBytes;
|
||||
try {
|
||||
// Prepare the output file path
|
||||
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) {
|
||||
String safeName = url.replaceAll("[^a-zA-Z0-9]", "_");
|
||||
if(safeName.length() > 50) {
|
||||
safeName = safeName.substring(0, 50); // restrict to 50 characters
|
||||
}
|
||||
return safeName + ".pdf";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package stirling.software.SPDF.controller.api.filters;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
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.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.PdfUtils;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Filter", description = "Filter APIs")
|
||||
public class FilterController {
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text")
|
||||
@Operation(summary = "Checks if a PDF contains set text, returns true if does", description = "Input:PDF Output:Boolean Type:SISO")
|
||||
public ResponseEntity<byte[]> containsText(
|
||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile,
|
||||
@Parameter(description = "The text to check for", required = true) String text,
|
||||
@Parameter(description = "The page number to check for text on accepts 'All', ranges like '1-4'", required = false) String pageNumber)
|
||||
throws IOException, InterruptedException {
|
||||
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
|
||||
if (PdfUtils.hasText(pdfDocument, pageNumber, text))
|
||||
return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename());
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image")
|
||||
@Operation(summary = "Checks if a PDF contains an image", description = "Input:PDF Output:Boolean Type:SISO")
|
||||
public ResponseEntity<byte[]> containsImage(
|
||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile,
|
||||
@Parameter(description = "The page number to check for image on accepts 'All', ranges like '1-4'", required = false) String pageNumber)
|
||||
throws IOException, InterruptedException {
|
||||
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")
|
||||
@Operation(summary = "Checks if a PDF is greater, less or equal to a setPageCount", description = "Input:PDF Output:Boolean Type:SISO")
|
||||
public ResponseEntity<byte[]> pageCount(
|
||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
|
||||
@Parameter(description = "Page Count", required = true) String pageCount,
|
||||
@Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
|
||||
"Greater", "Equal", "Less" })) String comparator)
|
||||
throws IOException, InterruptedException {
|
||||
// Load the PDF
|
||||
PDDocument document = PDDocument.load(inputFile.getInputStream());
|
||||
int actualPageCount = document.getNumberOfPages();
|
||||
|
||||
boolean valid = false;
|
||||
// Perform the comparison
|
||||
switch (comparator) {
|
||||
case "Greater":
|
||||
valid = actualPageCount > Integer.parseInt(pageCount);
|
||||
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)
|
||||
return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||
return null;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-size")
|
||||
@Operation(summary = "Checks if a PDF is of a certain size", description = "Input:PDF Output:Boolean Type:SISO")
|
||||
public ResponseEntity<byte[]> pageSize(
|
||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
|
||||
@Parameter(description = "Standard Page Size", required = true) String standardPageSize,
|
||||
@Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
|
||||
"Greater", "Equal", "Less" })) String comparator)
|
||||
throws IOException, InterruptedException {
|
||||
|
||||
// Load the PDF
|
||||
PDDocument document = PDDocument.load(inputFile.getInputStream());
|
||||
|
||||
PDPage firstPage = document.getPage(0);
|
||||
PDRectangle actualPageSize = firstPage.getMediaBox();
|
||||
|
||||
// Calculate the area of the actual page size
|
||||
float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight();
|
||||
|
||||
// Get the standard size and calculate its area
|
||||
PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize);
|
||||
float standardArea = standardSize.getWidth() * standardSize.getHeight();
|
||||
|
||||
boolean valid = false;
|
||||
// Perform the comparison
|
||||
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)
|
||||
return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||
return null;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-file-size")
|
||||
@Operation(summary = "Checks if a PDF is a set file size", description = "Input:PDF Output:Boolean Type:SISO")
|
||||
public ResponseEntity<byte[]> fileSize(
|
||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
|
||||
@Parameter(description = "File Size", required = true) String fileSize,
|
||||
@Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
|
||||
"Greater", "Equal", "Less" })) String comparator)
|
||||
throws IOException, InterruptedException {
|
||||
|
||||
// Get the file size
|
||||
long actualFileSize = inputFile.getSize();
|
||||
|
||||
boolean valid = false;
|
||||
// Perform the comparison
|
||||
switch (comparator) {
|
||||
case "Greater":
|
||||
valid = actualFileSize > Long.parseLong(fileSize);
|
||||
break;
|
||||
case "Equal":
|
||||
valid = actualFileSize == Long.parseLong(fileSize);
|
||||
break;
|
||||
case "Less":
|
||||
valid = actualFileSize < Long.parseLong(fileSize);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
||||
}
|
||||
|
||||
if (valid)
|
||||
return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||
return null;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation")
|
||||
@Operation(summary = "Checks if a PDF is of a certain rotation", description = "Input:PDF Output:Boolean Type:SISO")
|
||||
public ResponseEntity<byte[]> pageRotation(
|
||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
|
||||
@Parameter(description = "Rotation in degrees", required = true) int rotation,
|
||||
@Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
|
||||
"Greater", "Equal", "Less" })) String comparator)
|
||||
throws IOException, InterruptedException {
|
||||
|
||||
// Load the PDF
|
||||
PDDocument document = PDDocument.load(inputFile.getInputStream());
|
||||
|
||||
// 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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package stirling.software.SPDF.controller.api.other;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.apache.pdfbox.text.TextPosition;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
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.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@RestController
|
||||
@Tag(name = "Other", description = "Other APIs")
|
||||
public class AutoRenameController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AutoRenameController.class);
|
||||
|
||||
private static final float TITLE_FONT_SIZE_THRESHOLD = 20.0f;
|
||||
private static final int LINE_LIMIT = 11;
|
||||
|
||||
@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")
|
||||
public ResponseEntity<byte[]> extractHeader(
|
||||
@RequestPart(value = "fileInput") @Parameter(description = "The input PDF file from which the header is to be extracted.", required = true) MultipartFile file,
|
||||
@RequestParam(required = false, defaultValue = "false") @Parameter(description = "Flag indicating whether to use the first text as a fallback if no suitable title is found. Defaults to false.", required = false) Boolean useFirstTextAsFallback)
|
||||
throws Exception {
|
||||
|
||||
PDDocument document = PDDocument.load(file.getInputStream());
|
||||
PDFTextStripper reader = new PDFTextStripper() {
|
||||
class LineInfo {
|
||||
String text;
|
||||
float fontSize;
|
||||
|
||||
LineInfo(String text, float fontSize) {
|
||||
this.text = text;
|
||||
this.fontSize = fontSize;
|
||||
}
|
||||
}
|
||||
|
||||
List<LineInfo> lineInfos = new ArrayList<>();
|
||||
StringBuilder lineBuilder = new StringBuilder();
|
||||
float lastY = -1;
|
||||
float maxFontSizeInLine = 0.0f;
|
||||
int lineCount = 0;
|
||||
|
||||
@Override
|
||||
protected void processTextPosition(TextPosition text) {
|
||||
if (lastY != text.getY() && lineCount < LINE_LIMIT) {
|
||||
processLine();
|
||||
lineBuilder = new StringBuilder(text.getUnicode());
|
||||
maxFontSizeInLine = text.getFontSizeInPt();
|
||||
lastY = text.getY();
|
||||
lineCount++;
|
||||
} else if (lineCount < LINE_LIMIT) {
|
||||
lineBuilder.append(text.getUnicode());
|
||||
if (text.getFontSizeInPt() > maxFontSizeInLine) {
|
||||
maxFontSizeInLine = text.getFontSizeInPt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processLine() {
|
||||
if (lineBuilder.length() > 0 && lineCount < LINE_LIMIT) {
|
||||
lineInfos.add(new LineInfo(lineBuilder.toString(), maxFontSizeInLine));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText(PDDocument doc) throws IOException {
|
||||
this.lineInfos.clear();
|
||||
this.lineBuilder = new StringBuilder();
|
||||
this.lastY = -1;
|
||||
this.maxFontSizeInLine = 0.0f;
|
||||
this.lineCount = 0;
|
||||
super.getText(doc);
|
||||
processLine(); // Process the last line
|
||||
|
||||
// Merge lines with same font size
|
||||
List<LineInfo> mergedLineInfos = new ArrayList<>();
|
||||
for (int i = 0; i < lineInfos.size(); i++) {
|
||||
String mergedText = lineInfos.get(i).text;
|
||||
float fontSize = lineInfos.get(i).fontSize;
|
||||
while (i + 1 < lineInfos.size() && lineInfos.get(i + 1).fontSize == fontSize) {
|
||||
mergedText += " " + lineInfos.get(i + 1).text;
|
||||
i++;
|
||||
}
|
||||
mergedLineInfos.add(new LineInfo(mergedText, fontSize));
|
||||
}
|
||||
|
||||
// Sort lines by font size in descending order and get the first one
|
||||
mergedLineInfos.sort(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);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
String header = reader.getText(document);
|
||||
|
||||
|
||||
|
||||
// Sanitize the header string by removing characters not allowed in a filename.
|
||||
if (header != null && header.length() < 255) {
|
||||
header = header.replaceAll("[/\\\\?%*:|\"<>]", "");
|
||||
return WebResponseUtils.pdfDocToWebResponse(document, header + ".pdf");
|
||||
} else {
|
||||
logger.info("File has no good title to be found");
|
||||
return WebResponseUtils.pdfDocToWebResponse(document, file.getOriginalFilename());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package stirling.software.SPDF.controller.api.other;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferByte;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
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.rendering.PDFRenderer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.google.zxing.BinaryBitmap;
|
||||
import com.google.zxing.LuminanceSource;
|
||||
import com.google.zxing.MultiFormatReader;
|
||||
import com.google.zxing.NotFoundException;
|
||||
import com.google.zxing.PlanarYUVLuminanceSource;
|
||||
import com.google.zxing.Result;
|
||||
import com.google.zxing.common.HybridBinarizer;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
public class AutoSplitPdfController {
|
||||
|
||||
private static final String QR_CONTENT = "https://github.com/Frooodle/Stirling-PDF";
|
||||
|
||||
@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")
|
||||
public ResponseEntity<byte[]> autoSplitPdf(
|
||||
@RequestParam("fileInput") @Parameter(description = "The input PDF file which needs to be split into separate documents based on QR code boundaries.", required = true) MultipartFile file,
|
||||
@RequestParam(value ="duplexMode",defaultValue = "false") @Parameter(description = "Flag indicating if the duplex mode is active, where the page after the divider also gets removed.", required = false) boolean duplexMode)
|
||||
throws IOException {
|
||||
|
||||
InputStream inputStream = file.getInputStream();
|
||||
PDDocument document = PDDocument.load(inputStream);
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
|
||||
List<PDDocument> splitDocuments = new ArrayList<>();
|
||||
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
|
||||
|
||||
for (int page = 0; page < document.getNumberOfPages(); ++page) {
|
||||
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 150);
|
||||
String result = decodeQRCode(bim);
|
||||
|
||||
if (QR_CONTENT.equals(result) && page != 0) {
|
||||
splitDocuments.add(new PDDocument());
|
||||
}
|
||||
|
||||
if (!splitDocuments.isEmpty() && !QR_CONTENT.equals(result)) {
|
||||
splitDocuments.get(splitDocuments.size() - 1).addPage(document.getPage(page));
|
||||
} else if (page == 0) {
|
||||
PDDocument firstDocument = new PDDocument();
|
||||
firstDocument.addPage(document.getPage(page));
|
||||
splitDocuments.add(firstDocument);
|
||||
}
|
||||
|
||||
// If duplexMode is true and current page is a divider, then skip next page
|
||||
if (duplexMode && QR_CONTENT.equals(result)) {
|
||||
page++;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove split documents that have no pages
|
||||
splitDocuments.removeIf(pdDocument -> pdDocument.getNumberOfPages() == 0);
|
||||
|
||||
for (PDDocument splitDocument : splitDocuments) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
splitDocument.save(baos);
|
||||
splitDocumentsBoas.add(baos);
|
||||
splitDocument.close();
|
||||
}
|
||||
|
||||
document.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 static String decodeQRCode(BufferedImage bufferedImage) {
|
||||
LuminanceSource source;
|
||||
|
||||
if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte) {
|
||||
byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData();
|
||||
source = new PlanarYUVLuminanceSource(pixels, bufferedImage.getWidth(), bufferedImage.getHeight(), 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight(), false);
|
||||
} else if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferInt) {
|
||||
int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
|
||||
byte[] newPixels = new byte[pixels.length];
|
||||
for (int i = 0; i < pixels.length; i++) {
|
||||
newPixels[i] = (byte) (pixels[i] & 0xff);
|
||||
}
|
||||
source = new PlanarYUVLuminanceSource(newPixels, bufferedImage.getWidth(), bufferedImage.getHeight(), 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight(), false);
|
||||
} 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");
|
||||
}
|
||||
|
||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
|
||||
try {
|
||||
Result result = new MultiFormatReader().decode(bitmap);
|
||||
return result.getText();
|
||||
} catch (NotFoundException e) {
|
||||
return null; // there is no QR code in the image
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,17 +28,20 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import stirling.software.SPDF.pdf.ImageFinder;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.PdfUtils;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Other", description = "Other APIs")
|
||||
public class BlankPageController {
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-blanks")
|
||||
@Operation(
|
||||
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."
|
||||
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(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@@ -71,7 +74,7 @@ public class BlankPageController {
|
||||
pagesToKeepIndex.add(pageIndex);
|
||||
System.out.println("page " + pageIndex + " has text");
|
||||
} else {
|
||||
boolean hasImages = hasImagesOnPage(page);
|
||||
boolean hasImages = PdfUtils.hasImagesOnPage(page);
|
||||
if (hasImages) {
|
||||
System.out.println("page " + pageIndex + " has image");
|
||||
|
||||
@@ -84,10 +87,10 @@ public class BlankPageController {
|
||||
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
|
||||
int returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV).runCommandWithOutputHandling(command);
|
||||
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV).runCommandWithOutputHandling(command);
|
||||
|
||||
// does contain data
|
||||
if (returnCode == 0) {
|
||||
if (returnCode.getRc() == 0) {
|
||||
System.out.println("page " + pageIndex + " has image which is not blank");
|
||||
pagesToKeepIndex.add(pageIndex);
|
||||
} else {
|
||||
@@ -120,9 +123,5 @@ public class BlankPageController {
|
||||
}
|
||||
|
||||
|
||||
private static boolean hasImagesOnPage(PDPage page) throws IOException {
|
||||
ImageFinder imageFinder = new ImageFinder(page);
|
||||
imageFinder.processPage(page);
|
||||
return imageFinder.hasImages();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,17 +31,20 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Other", description = "Other APIs")
|
||||
public class CompressController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CompressController.class);
|
||||
|
||||
@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.")
|
||||
@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")
|
||||
public ResponseEntity<byte[]> optimizePdf(
|
||||
@RequestPart(value = "fileInput") @Parameter(description = "The input PDF file to be optimized.", required = true) MultipartFile inputFile,
|
||||
@RequestParam(required = false, value = "optimizeLevel") @Parameter(description = "The level of optimization to apply to the PDF file. Higher values indicate greater compression but may reduce quality.", schema = @Schema(allowableValues = {
|
||||
@@ -114,7 +117,7 @@ public class CompressController {
|
||||
command.add("-sOutputFile=" + tempOutputFile.toString());
|
||||
command.add(tempInputFile.toString());
|
||||
|
||||
int 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
|
||||
long outputFileSize = Files.size(tempOutputFile);
|
||||
@@ -219,6 +222,15 @@ public class CompressController {
|
||||
// Read the optimized PDF file
|
||||
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||
|
||||
// Check if optimized file is larger than the original
|
||||
if(pdfBytes.length > inputFileSize) {
|
||||
// Log the occurrence
|
||||
logger.warn("Optimized file is larger than the original. Returning the original file instead.");
|
||||
|
||||
// Read the original file again
|
||||
pdfBytes = Files.readAllBytes(tempInputFile);
|
||||
}
|
||||
|
||||
// Clean up the temporary files
|
||||
Files.delete(tempInputFile);
|
||||
Files.delete(tempOutputFile);
|
||||
|
||||
@@ -31,17 +31,20 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Other", description = "Other APIs")
|
||||
public class ExtractImageScansController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ExtractImageScansController.class);
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans")
|
||||
@Operation(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.")
|
||||
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(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@Parameter(description = "The input file containing image scans")
|
||||
@@ -115,7 +118,7 @@ public class ExtractImageScansController {
|
||||
|
||||
|
||||
// Run CLI command
|
||||
int 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
|
||||
List<Path> tempOutputFiles = Files.list(tempDir).sorted().collect(Collectors.toList());
|
||||
|
||||
@@ -29,15 +29,17 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@RestController
|
||||
@Tag(name = "Other", description = "Other APIs")
|
||||
public class ExtractImagesController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ExtractImagesController.class);
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/extract-images")
|
||||
@Operation(summary = "Extract images from a PDF file",
|
||||
description = "This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format.")
|
||||
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(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@Parameter(description = "The input PDF file containing images")
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
package stirling.software.SPDF.controller.api.other;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.image.AffineTransformOp;
|
||||
//Required for image manipulation
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.BufferedImageOp;
|
||||
import java.awt.image.ConvolveOp;
|
||||
import java.awt.image.Kernel;
|
||||
import java.awt.image.RescaleOp;
|
||||
//Required for file input/output
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
//Other required classes
|
||||
import java.util.Random;
|
||||
|
||||
//Required for image input/output
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
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.image.LosslessFactory;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -17,46 +36,17 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.itextpdf.io.source.ByteArrayOutputStream;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
//Required for PDF manipulation
|
||||
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.image.PDImageXObject;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
|
||||
import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
|
||||
|
||||
//Required for image manipulation
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.BufferedImageOp;
|
||||
import java.awt.image.RescaleOp;
|
||||
import java.awt.image.AffineTransformOp;
|
||||
import java.awt.image.ConvolveOp;
|
||||
import java.awt.image.Kernel;
|
||||
import java.awt.Color;
|
||||
import java.awt.geom.AffineTransform;
|
||||
|
||||
//Required for image input/output
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
//Required for file input/output
|
||||
import java.io.File;
|
||||
|
||||
//Other required classes
|
||||
import java.util.Random;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
@RestController
|
||||
public class FakeScanController {
|
||||
@Tag(name = "Other", description = "Other APIs")
|
||||
public class FakeScanControllerWIP {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(FakeScanController.class);
|
||||
private static final Logger logger = LoggerFactory.getLogger(FakeScanControllerWIP.class);
|
||||
|
||||
//TODO
|
||||
@Hidden
|
||||
@@ -19,9 +19,11 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Other", description = "Other APIs")
|
||||
public class MetadataController {
|
||||
|
||||
|
||||
@@ -38,7 +40,7 @@ public class MetadataController {
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/update-metadata")
|
||||
@Operation(summary = "Update metadata of a PDF file",
|
||||
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.")
|
||||
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(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@Parameter(description = "The input PDF file to update metadata")
|
||||
@@ -73,6 +75,7 @@ public class MetadataController {
|
||||
@RequestParam(value = "trapped", required = false)
|
||||
@Parameter(description = "The trapped status of the document")
|
||||
String trapped,
|
||||
@Parameter(description = "Map list of key and value of custom parameters, note these must start with customKey and customValue if they are non standard")
|
||||
@RequestParam Map<String, String> allRequestParams)
|
||||
throws IOException {
|
||||
|
||||
|
||||
@@ -27,10 +27,13 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Other", description = "Other APIs")
|
||||
public class OCRController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OCRController.class);
|
||||
@@ -47,7 +50,7 @@ public class OCRController {
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
|
||||
@Operation(summary = "Process a PDF file with OCR",
|
||||
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.")
|
||||
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(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@Parameter(description = "The input PDF file to be processed with OCR")
|
||||
@@ -139,8 +142,12 @@ public class OCRController {
|
||||
command.addAll(Arrays.asList("--language", languageOption, tempInputFile.toString(), tempOutputFile.toString()));
|
||||
|
||||
// Run CLI command
|
||||
int returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command);
|
||||
|
||||
ProcessExecutorResult result = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command);
|
||||
if(result.getRc() != 0 && result.getMessages().contains("multiprocessing/synchronize.py") && result.getMessages().contains("OSError: [Errno 38] Function not implemented")) {
|
||||
command.add("--jobs");
|
||||
command.add("1");
|
||||
result = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -151,7 +158,7 @@ public class OCRController {
|
||||
|
||||
List<String> gsCommand = Arrays.asList("gs", "-sDEVICE=pdfwrite", "-dFILTERIMAGE", "-o", tempPdfWithoutImages.toString(), tempOutputFile.toString());
|
||||
|
||||
int gsReturnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(gsCommand);
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(gsCommand);
|
||||
tempOutputFile = tempPdfWithoutImages;
|
||||
}
|
||||
// Read the OCR processed PDF file
|
||||
|
||||
@@ -14,10 +14,12 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.PdfUtils;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Other", description = "Other APIs")
|
||||
public class OverlayImageController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(OverlayImageController.class);
|
||||
@@ -25,7 +27,7 @@ public class OverlayImageController {
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/add-image")
|
||||
@Operation(
|
||||
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."
|
||||
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(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package stirling.software.SPDF.controller.api.other;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.itextpdf.io.font.constants.StandardFonts;
|
||||
import com.itextpdf.kernel.font.PdfFont;
|
||||
import com.itextpdf.kernel.font.PdfFontFactory;
|
||||
import com.itextpdf.kernel.geom.Rectangle;
|
||||
import com.itextpdf.kernel.pdf.PdfDocument;
|
||||
import com.itextpdf.kernel.pdf.PdfPage;
|
||||
import com.itextpdf.kernel.pdf.PdfReader;
|
||||
import com.itextpdf.kernel.pdf.PdfWriter;
|
||||
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
|
||||
import com.itextpdf.layout.Canvas;
|
||||
import com.itextpdf.layout.element.Paragraph;
|
||||
import com.itextpdf.layout.properties.TextAlignment;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Other", description = "Other APIs")
|
||||
public class PageNumbersController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class);
|
||||
|
||||
@PostMapping(value = "/add-page-numbers", consumes = "multipart/form-data")
|
||||
@Operation(summary = "Add page numbers to a PDF document", description = "This operation takes an input PDF file and adds page numbers to it. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> addPageNumbers(
|
||||
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
|
||||
@Parameter(description = "Custom margin: small/medium/large", required = true, schema = @Schema(type = "string", allowableValues = {"small", "medium", "large"})) @RequestParam("customMargin") String customMargin,
|
||||
@Parameter(description = "Position: 1 of 9 positions", required = true, schema = @Schema(type = "integer", minimum = "1", maximum = "9")) @RequestParam("position") int position,
|
||||
@Parameter(description = "Starting number", required = true, schema = @Schema(type = "integer", minimum = "1")) @RequestParam("startingNumber") int startingNumber,
|
||||
@Parameter(description = "Which pages to number, default all", required = false, schema = @Schema(type = "string")) @RequestParam(value = "pagesToNumber", required = false) String pagesToNumber,
|
||||
@Parameter(description = "Custom text: defaults to just number but can have things like \"Page {n} of {p}\"", required = false, schema = @Schema(type = "string")) @RequestParam(value = "customText", required = false) String customText)
|
||||
throws IOException {
|
||||
|
||||
byte[] fileBytes = file.getBytes();
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
|
||||
|
||||
int pageNumber = startingNumber;
|
||||
float marginFactor;
|
||||
switch (customMargin.toLowerCase()) {
|
||||
case "small":
|
||||
marginFactor = 0.02f;
|
||||
break;
|
||||
case "medium":
|
||||
marginFactor = 0.035f;
|
||||
break;
|
||||
case "large":
|
||||
marginFactor = 0.05f;
|
||||
break;
|
||||
case "x-large":
|
||||
marginFactor = 0.1f;
|
||||
break;
|
||||
default:
|
||||
marginFactor = 0.035f;
|
||||
break;
|
||||
}
|
||||
|
||||
float fontSize = 12.0f;
|
||||
|
||||
PdfReader reader = new PdfReader(bais);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
PdfWriter writer = new PdfWriter(baos);
|
||||
|
||||
PdfDocument pdfDoc = new PdfDocument(reader, writer);
|
||||
|
||||
List<Integer> pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), pdfDoc.getNumberOfPages());
|
||||
|
||||
for (int i : pagesToNumberList) {
|
||||
PdfPage page = pdfDoc.getPage(i+1);
|
||||
Rectangle pageSize = page.getPageSize();
|
||||
PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamAfter(), page.getResources(), pdfDoc);
|
||||
|
||||
String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(pdfDoc.getNumberOfPages())).replace("{filename}", file.getOriginalFilename().replaceFirst("[.][^.]+$", "")) : String.valueOf(pageNumber);
|
||||
|
||||
PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA);
|
||||
float textWidth = font.getWidth(text, fontSize);
|
||||
float textHeight = font.getAscent(text, fontSize) - font.getDescent(text, fontSize);
|
||||
|
||||
float x, y;
|
||||
TextAlignment alignment;
|
||||
|
||||
int xGroup = (position - 1) % 3;
|
||||
int yGroup = 2 - (position - 1) / 3;
|
||||
|
||||
switch (xGroup) {
|
||||
case 0: // left
|
||||
x = pageSize.getLeft() + marginFactor * pageSize.getWidth();
|
||||
alignment = TextAlignment.LEFT;
|
||||
break;
|
||||
case 1: // center
|
||||
x = pageSize.getLeft() + (pageSize.getWidth()) / 2;
|
||||
alignment = TextAlignment.CENTER;
|
||||
break;
|
||||
default: // right
|
||||
x = pageSize.getRight() - marginFactor * pageSize.getWidth();
|
||||
alignment = TextAlignment.RIGHT;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (yGroup) {
|
||||
case 0: // bottom
|
||||
y = pageSize.getBottom() + marginFactor * pageSize.getHeight();
|
||||
break;
|
||||
case 1: // middle
|
||||
y = pageSize.getBottom() + (pageSize.getHeight() ) / 2;
|
||||
break;
|
||||
default: // top
|
||||
y = pageSize.getTop() - marginFactor * pageSize.getHeight();
|
||||
break;
|
||||
}
|
||||
|
||||
new Canvas(pdfCanvas, page.getPageSize())
|
||||
.showTextAligned(new Paragraph(text).setFont(font).setFontSize(fontSize), x, y, alignment);
|
||||
|
||||
pageNumber++;
|
||||
}
|
||||
|
||||
|
||||
pdfDoc.close();
|
||||
byte[] resultBytes = baos.toByteArray();
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(resultBytes, URLEncoder.encode(file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", "UTF-8"), MediaType.APPLICATION_PDF);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -16,10 +16,13 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Other", description = "Other APIs")
|
||||
public class RepairController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RepairController.class);
|
||||
@@ -27,7 +30,7 @@ public class RepairController {
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/repair")
|
||||
@Operation(
|
||||
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. Input:PDF Output:PDF Type:SISO"
|
||||
)
|
||||
public ResponseEntity<byte[]> repairPdf(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@@ -49,7 +52,7 @@ public class RepairController {
|
||||
command.add(tempInputFile.toString());
|
||||
|
||||
|
||||
int returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(command);
|
||||
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(command);
|
||||
|
||||
// Read the optimized PDF file
|
||||
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package stirling.software.SPDF.controller.api.other;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.itextpdf.kernel.pdf.PdfArray;
|
||||
import com.itextpdf.kernel.pdf.PdfDictionary;
|
||||
import com.itextpdf.kernel.pdf.PdfDocument;
|
||||
import com.itextpdf.kernel.pdf.PdfName;
|
||||
import com.itextpdf.kernel.pdf.PdfObject;
|
||||
import com.itextpdf.kernel.pdf.PdfReader;
|
||||
import com.itextpdf.kernel.pdf.PdfStream;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@RestController
|
||||
@Tag(name = "Other", description = "Other APIs")
|
||||
public class ShowJavascript {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class);
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/show-javascript")
|
||||
@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")
|
||||
public ResponseEntity<byte[]> extractHeader(
|
||||
@RequestPart(value = "fileInput") @Parameter(description = "The input PDF file from which the javascript is to be extracted.", required = true) MultipartFile inputFile)
|
||||
throws Exception {
|
||||
|
||||
try (
|
||||
PdfDocument itextDoc = new PdfDocument(new PdfReader(inputFile.getInputStream()))
|
||||
) {
|
||||
|
||||
String name = "";
|
||||
String script = "";
|
||||
String entryName = "File: "+inputFile.getOriginalFilename() + ", Script: ";
|
||||
//Javascript
|
||||
PdfDictionary namesDict = itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names);
|
||||
if (namesDict != null) {
|
||||
PdfDictionary javascriptDict = namesDict.getAsDictionary(PdfName.JavaScript);
|
||||
if (javascriptDict != null) {
|
||||
|
||||
PdfArray namesArray = javascriptDict.getAsArray(PdfName.Names);
|
||||
for (int i = 0; i < namesArray.size(); i += 2) {
|
||||
if(namesArray.getAsString(i) != null)
|
||||
name = namesArray.getAsString(i).toString();
|
||||
|
||||
PdfObject jsCode = namesArray.get(i+1);
|
||||
if (jsCode instanceof PdfStream) {
|
||||
byte[] jsCodeBytes = ((PdfStream)jsCode).getBytes();
|
||||
String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8);
|
||||
script = "//" + entryName + name + "\n" +jsCodeStr;
|
||||
|
||||
} else if (jsCode instanceof PdfDictionary) {
|
||||
// If the JS code is in a dictionary, you'll need to know the key to use.
|
||||
// Assuming the key is PdfName.JS:
|
||||
PdfStream jsCodeStream = ((PdfDictionary)jsCode).getAsStream(PdfName.JS);
|
||||
if (jsCodeStream != null) {
|
||||
byte[] jsCodeBytes = jsCodeStream.getBytes();
|
||||
String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8);
|
||||
script = "//" + entryName + name + "\n" +jsCodeStr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if(script.equals("")) {
|
||||
script = "PDF '" +inputFile.getOriginalFilename() + "' does not contain Javascript";
|
||||
}
|
||||
return WebResponseUtils.bytesToWebResponse(script.getBytes(), name + ".js");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
package stirling.software.SPDF.controller.api.pipeline;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
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.Map;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
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.scheduling.annotation.Scheduled;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
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.client.RestTemplate;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.PipelineConfig;
|
||||
import stirling.software.SPDF.model.PipelineOperation;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Pipeline", description = "Pipeline APIs")
|
||||
public class PipelineController {
|
||||
|
||||
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 finishedFoldersDir = "./pipeline/finishedFolders/";
|
||||
|
||||
@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
|
||||
ApplicationProperties applicationProperties;
|
||||
|
||||
|
||||
private void handleDirectory(Path dir) throws Exception {
|
||||
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)) {
|
||||
// Read JSON file
|
||||
String jsonString;
|
||||
try {
|
||||
jsonString = new String(Files.readAllBytes(jsonFile));
|
||||
logger.info("Read JSON file: {}", jsonFile);
|
||||
} catch (IOException e) {
|
||||
logger.error("Error reading JSON file: {}", jsonFile, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Decode JSON to PipelineConfig
|
||||
PipelineConfig config;
|
||||
try {
|
||||
config = objectMapper.readValue(jsonString, PipelineConfig.class);
|
||||
// Assuming your PipelineConfig class has getters for all necessary fields, you
|
||||
// can perform checks here
|
||||
if (config.getOperations() == null || config.getOutputDir() == null || config.getName() == null) {
|
||||
throw new IOException("Invalid JSON format");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Error parsing PipelineConfig: {}", jsonString, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// For each operation in the pipeline
|
||||
for (PipelineOperation operation : config.getOperations()) {
|
||||
// Collect all files based on fileInput
|
||||
File[] files;
|
||||
String fileInput = (String) operation.getParameters().get("fileInput");
|
||||
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) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// If fileInput contains a path, process only this file
|
||||
files = new File[] { new File(fileInput) };
|
||||
}
|
||||
|
||||
// Prepare the files for processing
|
||||
List<File> filesToProcess = new ArrayList<>();
|
||||
for (File file : files) {
|
||||
logger.info(file.getName());
|
||||
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
|
||||
try {
|
||||
List<Resource> resources = handleFiles(filesToProcess.toArray(new File[0]), jsonString);
|
||||
|
||||
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();
|
||||
|
||||
if (outputFolder == null || outputFolder.isEmpty()) {
|
||||
// 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
|
||||
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(@RequestPart("fileInput") MultipartFile[] files,
|
||||
@RequestParam("json") String jsonString) {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -51,8 +51,10 @@ import com.itextpdf.signatures.SignatureUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@RestController
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
public class CertSignController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CertSignController.class);
|
||||
@@ -63,7 +65,7 @@ public class CertSignController {
|
||||
|
||||
@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.")
|
||||
description = "This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:MF-SISO")
|
||||
public ResponseEntity<byte[]> signPDF(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@Parameter(description = "The input PDF file to be signed")
|
||||
|
||||
@@ -0,0 +1,763 @@
|
||||
package stirling.software.SPDF.controller.api.security;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.pdfbox.cos.COSString;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
||||
import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement;
|
||||
import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureNode;
|
||||
import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot;
|
||||
import org.apache.pdfbox.pdmodel.encryption.PDEncryption;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.itextpdf.forms.PdfAcroForm;
|
||||
import com.itextpdf.forms.fields.PdfFormField;
|
||||
import com.itextpdf.kernel.geom.Rectangle;
|
||||
import com.itextpdf.kernel.pdf.PdfArray;
|
||||
import com.itextpdf.kernel.pdf.PdfCatalog;
|
||||
import com.itextpdf.kernel.pdf.PdfDictionary;
|
||||
import com.itextpdf.kernel.pdf.PdfDocument;
|
||||
import com.itextpdf.kernel.pdf.PdfName;
|
||||
import com.itextpdf.kernel.pdf.PdfObject;
|
||||
import com.itextpdf.kernel.pdf.PdfOutline;
|
||||
import com.itextpdf.kernel.pdf.PdfReader;
|
||||
import com.itextpdf.kernel.pdf.PdfResources;
|
||||
import com.itextpdf.kernel.pdf.PdfStream;
|
||||
import com.itextpdf.kernel.pdf.PdfString;
|
||||
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
|
||||
import com.itextpdf.kernel.pdf.annot.PdfFileAttachmentAnnotation;
|
||||
import com.itextpdf.kernel.pdf.annot.PdfLinkAnnotation;
|
||||
import com.itextpdf.kernel.pdf.layer.PdfLayer;
|
||||
import com.itextpdf.kernel.pdf.layer.PdfOCProperties;
|
||||
import com.itextpdf.kernel.xmp.XMPException;
|
||||
import com.itextpdf.kernel.xmp.XMPMeta;
|
||||
import com.itextpdf.kernel.xmp.XMPMetaFactory;
|
||||
import com.itextpdf.kernel.xmp.options.SerializeOptions;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@RestController
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
public class GetInfoOnPDF {
|
||||
|
||||
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")
|
||||
public ResponseEntity<byte[]> getPdfInfo(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@Parameter(description = "The input PDF file to get info on", required = true) MultipartFile inputFile)
|
||||
throws IOException {
|
||||
|
||||
try (
|
||||
PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream());
|
||||
PdfDocument itextDoc = new PdfDocument(new PdfReader(inputFile.getInputStream()))
|
||||
) {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
ObjectNode jsonOutput = objectMapper.createObjectNode();
|
||||
|
||||
// Metadata using PDFBox
|
||||
PDDocumentInformation info = pdfBoxDoc.getDocumentInformation();
|
||||
ObjectNode metadata = objectMapper.createObjectNode();
|
||||
ObjectNode basicInfo = objectMapper.createObjectNode();
|
||||
ObjectNode docInfoNode = objectMapper.createObjectNode();
|
||||
ObjectNode compliancy = objectMapper.createObjectNode();
|
||||
ObjectNode encryption = objectMapper.createObjectNode();
|
||||
ObjectNode other = objectMapper.createObjectNode();
|
||||
|
||||
|
||||
metadata.put("Title", info.getTitle());
|
||||
metadata.put("Author", info.getAuthor());
|
||||
metadata.put("Subject", info.getSubject());
|
||||
metadata.put("Keywords", info.getKeywords());
|
||||
metadata.put("Producer", info.getProducer());
|
||||
metadata.put("Creator", info.getCreator());
|
||||
metadata.put("CreationDate", formatDate(info.getCreationDate()));
|
||||
metadata.put("ModificationDate", formatDate(info.getModificationDate()));
|
||||
jsonOutput.set("Metadata", metadata);
|
||||
|
||||
|
||||
|
||||
|
||||
// Total file size of the PDF
|
||||
long fileSizeInBytes = inputFile.getSize();
|
||||
basicInfo.put("FileSizeInBytes", fileSizeInBytes);
|
||||
|
||||
// Number of words, paragraphs, and images in the entire document
|
||||
String fullText = new PDFTextStripper().getText(pdfBoxDoc);
|
||||
String[] words = fullText.split("\\s+");
|
||||
int wordCount = words.length;
|
||||
int paragraphCount = fullText.split("\r\n|\r|\n").length;
|
||||
basicInfo.put("WordCount", wordCount);
|
||||
basicInfo.put("ParagraphCount", paragraphCount);
|
||||
// Number of characters in the entire document (including spaces and special characters)
|
||||
int charCount = fullText.length();
|
||||
basicInfo.put("CharacterCount", charCount);
|
||||
|
||||
|
||||
// Initialize the flags and types
|
||||
boolean hasCompression = false;
|
||||
String compressionType = "None";
|
||||
|
||||
// Check for object streams
|
||||
for (int i = 1; i <= itextDoc.getNumberOfPdfObjects(); i++) {
|
||||
PdfObject obj = itextDoc.getPdfObject(i);
|
||||
if (obj != null && obj.isStream() && ((PdfStream) obj).get(PdfName.Type) == PdfName.ObjStm) {
|
||||
hasCompression = true;
|
||||
compressionType = "Object Streams";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If not compressed using object streams, check for compressed Xref tables
|
||||
if (!hasCompression && itextDoc.getReader().hasRebuiltXref()) {
|
||||
hasCompression = true;
|
||||
compressionType = "Compressed Xref or Rebuilt Xref";
|
||||
}
|
||||
basicInfo.put("Compression", hasCompression);
|
||||
if(hasCompression)
|
||||
basicInfo.put("CompressionType", compressionType);
|
||||
|
||||
String language = pdfBoxDoc.getDocumentCatalog().getLanguage();
|
||||
basicInfo.put("Language", language);
|
||||
basicInfo.put("Number of pages", pdfBoxDoc.getNumberOfPages());
|
||||
|
||||
|
||||
// Page Mode using iText7
|
||||
PdfCatalog catalog = itextDoc.getCatalog();
|
||||
PdfName pageMode = catalog.getPdfObject().getAsName(PdfName.PageMode);
|
||||
|
||||
// Document Information using PDFBox
|
||||
docInfoNode.put("PDF version", pdfBoxDoc.getVersion());
|
||||
docInfoNode.put("Trapped", info.getTrapped());
|
||||
docInfoNode.put("Page Mode", getPageModeDescription(pageMode));;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
PdfAcroForm acroForm = PdfAcroForm.getAcroForm(itextDoc, false);
|
||||
ObjectNode formFieldsNode = objectMapper.createObjectNode();
|
||||
if (acroForm != null) {
|
||||
for (Map.Entry<String, PdfFormField> entry : acroForm.getFormFields().entrySet()) {
|
||||
formFieldsNode.put(entry.getKey(), entry.getValue().getValueAsString());
|
||||
}
|
||||
}
|
||||
jsonOutput.set("FormFields", formFieldsNode);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//embeed files TODO size
|
||||
ArrayNode embeddedFilesArray = objectMapper.createArrayNode();
|
||||
if(itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names) != null)
|
||||
{
|
||||
PdfDictionary embeddedFiles = itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names)
|
||||
.getAsDictionary(PdfName.EmbeddedFiles);
|
||||
if (embeddedFiles != null) {
|
||||
|
||||
PdfArray namesArray = embeddedFiles.getAsArray(PdfName.Names);
|
||||
if(namesArray != null) {
|
||||
for (int i = 0; i < namesArray.size(); i += 2) {
|
||||
ObjectNode embeddedFileNode = objectMapper.createObjectNode();
|
||||
embeddedFileNode.put("Name", namesArray.getAsString(i).toString());
|
||||
// Add other details if required
|
||||
embeddedFilesArray.add(embeddedFileNode);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
other.set("EmbeddedFiles", embeddedFilesArray);
|
||||
|
||||
//attachments TODO size
|
||||
ArrayNode attachmentsArray = objectMapper.createArrayNode();
|
||||
for (int pageNum = 1; pageNum <= itextDoc.getNumberOfPages(); pageNum++) {
|
||||
for (PdfAnnotation annotation : itextDoc.getPage(pageNum).getAnnotations()) {
|
||||
if (annotation instanceof PdfFileAttachmentAnnotation) {
|
||||
ObjectNode attachmentNode = objectMapper.createObjectNode();
|
||||
attachmentNode.put("Name", ((PdfFileAttachmentAnnotation) annotation).getName().toString());
|
||||
attachmentNode.put("Description", annotation.getContents().getValue());
|
||||
attachmentsArray.add(attachmentNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
other.set("Attachments", attachmentsArray);
|
||||
|
||||
//Javascript
|
||||
PdfDictionary namesDict = itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names);
|
||||
ArrayNode javascriptArray = objectMapper.createArrayNode();
|
||||
if (namesDict != null) {
|
||||
PdfDictionary javascriptDict = namesDict.getAsDictionary(PdfName.JavaScript);
|
||||
if (javascriptDict != null) {
|
||||
|
||||
PdfArray namesArray = javascriptDict.getAsArray(PdfName.Names);
|
||||
for (int i = 0; i < namesArray.size(); i += 2) {
|
||||
ObjectNode jsNode = objectMapper.createObjectNode();
|
||||
if(namesArray.getAsString(i) != null)
|
||||
jsNode.put("JS Name", namesArray.getAsString(i).toString());
|
||||
|
||||
// Here we check for a PdfStream object and retrieve the JS code from it
|
||||
PdfObject jsCode = namesArray.get(i+1);
|
||||
if (jsCode instanceof PdfStream) {
|
||||
byte[] jsCodeBytes = ((PdfStream)jsCode).getBytes();
|
||||
String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8);
|
||||
jsNode.put("JS Script Length", jsCodeStr.length());
|
||||
} else if (jsCode instanceof PdfDictionary) {
|
||||
// If the JS code is in a dictionary, you'll need to know the key to use.
|
||||
// Assuming the key is PdfName.JS:
|
||||
PdfStream jsCodeStream = ((PdfDictionary)jsCode).getAsStream(PdfName.JS);
|
||||
if (jsCodeStream != null) {
|
||||
byte[] jsCodeBytes = jsCodeStream.getBytes();
|
||||
String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8);
|
||||
jsNode.put("JS Script Character Length", jsCodeStr.length());
|
||||
}
|
||||
}
|
||||
|
||||
javascriptArray.add(jsNode);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
other.set("JavaScript", javascriptArray);
|
||||
|
||||
//TODO size
|
||||
PdfOCProperties ocProperties = itextDoc.getCatalog().getOCProperties(false);
|
||||
ArrayNode layersArray = objectMapper.createArrayNode();
|
||||
if (ocProperties != null) {
|
||||
|
||||
for (PdfLayer layer : ocProperties.getLayers()) {
|
||||
ObjectNode layerNode = objectMapper.createObjectNode();
|
||||
layerNode.put("Name", layer.getPdfObject().getAsString(PdfName.Name).toString());
|
||||
layersArray.add(layerNode);
|
||||
}
|
||||
|
||||
}
|
||||
other.set("Layers", layersArray);
|
||||
|
||||
//TODO Security
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Digital Signatures using iText7 TODO
|
||||
|
||||
|
||||
|
||||
|
||||
PDStructureTreeRoot structureTreeRoot = pdfBoxDoc.getDocumentCatalog().getStructureTreeRoot();
|
||||
ArrayNode structureTreeArray;
|
||||
try {
|
||||
if(structureTreeRoot != null) {
|
||||
structureTreeArray = exploreStructureTree(structureTreeRoot.getKids());
|
||||
other.set("StructureTree", structureTreeArray);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
boolean isPdfACompliant = checkOutputIntent(itextDoc, "PDF/A");
|
||||
boolean isPdfXCompliant = checkOutputIntent(itextDoc, "PDF/X");
|
||||
boolean isPdfECompliant = checkForStandard(itextDoc, "PDF/E");
|
||||
boolean isPdfVTCompliant = checkForStandard(itextDoc, "PDF/VT");
|
||||
boolean isPdfUACompliant = checkForStandard(itextDoc, "PDF/UA");
|
||||
boolean isPdfBCompliant = checkForStandard(itextDoc, "PDF/B"); // If you want to check for PDF/Broadcast, though this isn't an official ISO standard.
|
||||
boolean isPdfSECCompliant = checkForStandard(itextDoc, "PDF/SEC"); // This might not be effective since PDF/SEC was under development in 2021.
|
||||
|
||||
compliancy.put("IsPDF/ACompliant", isPdfACompliant);
|
||||
compliancy.put("IsPDF/XCompliant", isPdfXCompliant);
|
||||
compliancy.put("IsPDF/ECompliant", isPdfECompliant);
|
||||
compliancy.put("IsPDF/VTCompliant", isPdfVTCompliant);
|
||||
compliancy.put("IsPDF/UACompliant", isPdfUACompliant);
|
||||
compliancy.put("IsPDF/BCompliant", isPdfBCompliant);
|
||||
compliancy.put("IsPDF/SECCompliant", isPdfSECCompliant);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
ArrayNode bookmarksArray = objectMapper.createArrayNode();
|
||||
PdfOutline root = itextDoc.getOutlines(false);
|
||||
if (root != null) {
|
||||
for (PdfOutline child : root.getAllChildren()) {
|
||||
addOutlinesToArray(child, bookmarksArray);
|
||||
}
|
||||
}
|
||||
other.set("Bookmarks/Outline/TOC", bookmarksArray);
|
||||
|
||||
byte[] xmpBytes = itextDoc.getXmpMetadata();
|
||||
String xmpString = null;
|
||||
if (xmpBytes != null) {
|
||||
try {
|
||||
XMPMeta xmpMeta = XMPMetaFactory.parseFromBuffer(xmpBytes);
|
||||
xmpString = new String(XMPMetaFactory.serializeToBuffer(xmpMeta, new SerializeOptions()));
|
||||
} catch (XMPException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
other.put("XMPMetadata", xmpString);
|
||||
|
||||
|
||||
|
||||
if (pdfBoxDoc.isEncrypted()) {
|
||||
encryption.put("IsEncrypted", true);
|
||||
|
||||
// Retrieve encryption details using getEncryption()
|
||||
PDEncryption pdfEncryption = pdfBoxDoc.getEncryption();
|
||||
encryption.put("EncryptionAlgorithm", pdfEncryption.getFilter());
|
||||
encryption.put("KeyLength", pdfEncryption.getLength());
|
||||
encryption.put("Permissions", pdfBoxDoc.getCurrentAccessPermission().toString());
|
||||
|
||||
// Add other encryption-related properties as needed
|
||||
} else {
|
||||
encryption.put("IsEncrypted", false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
ObjectNode pageInfoParent = objectMapper.createObjectNode();
|
||||
for (int pageNum = 1; pageNum <= itextDoc.getNumberOfPages(); pageNum++) {
|
||||
ObjectNode pageInfo = objectMapper.createObjectNode();
|
||||
|
||||
// Page-level Information
|
||||
Rectangle pageSize = itextDoc.getPage(pageNum).getPageSize();
|
||||
pageInfo.put("Width", pageSize.getWidth());
|
||||
pageInfo.put("Height", pageSize.getHeight());
|
||||
pageInfo.put("Rotation", itextDoc.getPage(pageNum).getRotation());
|
||||
pageInfo.put("Page Orientation", getPageOrientation(pageSize.getWidth(),pageSize.getHeight()));
|
||||
pageInfo.put("Standard Size", getPageSize(pageSize.getWidth(),pageSize.getHeight()));
|
||||
|
||||
// Boxes
|
||||
pageInfo.put("MediaBox", itextDoc.getPage(pageNum).getMediaBox().toString());
|
||||
pageInfo.put("CropBox", itextDoc.getPage(pageNum).getCropBox().toString());
|
||||
pageInfo.put("BleedBox", itextDoc.getPage(pageNum).getBleedBox().toString());
|
||||
pageInfo.put("TrimBox", itextDoc.getPage(pageNum).getTrimBox().toString());
|
||||
pageInfo.put("ArtBox", itextDoc.getPage(pageNum).getArtBox().toString());
|
||||
|
||||
// Content Extraction
|
||||
PDFTextStripper textStripper = new PDFTextStripper();
|
||||
textStripper.setStartPage(pageNum -1);
|
||||
textStripper.setEndPage(pageNum - 1);
|
||||
String pageText = textStripper.getText(pdfBoxDoc);
|
||||
|
||||
pageInfo.put("Text Characters Count", pageText.length()); //
|
||||
|
||||
// Annotations
|
||||
List<PdfAnnotation> annotations = itextDoc.getPage(pageNum).getAnnotations();
|
||||
|
||||
int subtypeCount = 0;
|
||||
int contentsCount = 0;
|
||||
|
||||
for (PdfAnnotation annotation : annotations) {
|
||||
if(annotation.getSubtype() != null) {
|
||||
subtypeCount++; // Increase subtype count
|
||||
}
|
||||
if(annotation.getContents() != null) {
|
||||
contentsCount++; // Increase contents count
|
||||
}
|
||||
}
|
||||
|
||||
ObjectNode annotationsObject = objectMapper.createObjectNode();
|
||||
annotationsObject.put("AnnotationsCount", annotations.size());
|
||||
annotationsObject.put("SubtypeCount", subtypeCount);
|
||||
annotationsObject.put("ContentsCount", contentsCount);
|
||||
pageInfo.set("Annotations", annotationsObject);
|
||||
|
||||
// Images (simplified)
|
||||
// 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.
|
||||
ArrayNode imagesArray = objectMapper.createArrayNode();
|
||||
PdfResources resources = itextDoc.getPage(pageNum).getResources();
|
||||
for (PdfName name : resources.getResourceNames()) {
|
||||
PdfObject obj = resources.getResource(name);
|
||||
if (obj instanceof PdfStream) {
|
||||
PdfStream stream = (PdfStream) obj;
|
||||
if (PdfName.Image.equals(stream.getAsName(PdfName.Subtype))) {
|
||||
ObjectNode imageNode = objectMapper.createObjectNode();
|
||||
imageNode.put("Width", stream.getAsNumber(PdfName.Width).intValue());
|
||||
imageNode.put("Height", stream.getAsNumber(PdfName.Height).intValue());
|
||||
PdfObject colorSpace = stream.get(PdfName.ColorSpace);
|
||||
if (colorSpace != null) {
|
||||
imageNode.put("ColorSpace", colorSpace.toString());
|
||||
}
|
||||
imagesArray.add(imageNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo.set("Images", imagesArray);
|
||||
|
||||
|
||||
// Links
|
||||
ArrayNode linksArray = objectMapper.createArrayNode();
|
||||
Set<String> uniqueURIs = new HashSet<>(); // To store unique URIs
|
||||
|
||||
for (PdfAnnotation annotation : annotations) {
|
||||
if (annotation instanceof PdfLinkAnnotation) {
|
||||
PdfLinkAnnotation linkAnnotation = (PdfLinkAnnotation) annotation;
|
||||
if(linkAnnotation != null && linkAnnotation.getAction() != null) {
|
||||
String uri = linkAnnotation.getAction().toString();
|
||||
uniqueURIs.add(uri); // Add to set to ensure uniqueness
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add unique URIs to linksArray
|
||||
for (String uri : uniqueURIs) {
|
||||
ObjectNode linkNode = objectMapper.createObjectNode();
|
||||
linkNode.put("URI", uri);
|
||||
linksArray.add(linkNode);
|
||||
}
|
||||
pageInfo.set("Links", linksArray);
|
||||
|
||||
// Fonts
|
||||
ArrayNode fontsArray = objectMapper.createArrayNode();
|
||||
PdfDictionary fontDicts = resources.getResource(PdfName.Font);
|
||||
Set<String> uniqueSubtypes = new HashSet<>(); // To store unique subtypes
|
||||
|
||||
// Map to store unique fonts and their counts
|
||||
Map<String, ObjectNode> uniqueFontsMap = new HashMap<>();
|
||||
|
||||
if (fontDicts != null) {
|
||||
for (PdfName key : fontDicts.keySet()) {
|
||||
ObjectNode fontNode = objectMapper.createObjectNode(); // Create a new font node for each font
|
||||
PdfDictionary font = fontDicts.getAsDictionary(key);
|
||||
|
||||
boolean isEmbedded = font.containsKey(PdfName.FontFile) ||
|
||||
font.containsKey(PdfName.FontFile2) ||
|
||||
font.containsKey(PdfName.FontFile3);
|
||||
fontNode.put("IsEmbedded", isEmbedded);
|
||||
|
||||
if (font.containsKey(PdfName.Encoding)) {
|
||||
String encoding = font.getAsName(PdfName.Encoding).toString();
|
||||
fontNode.put("Encoding", encoding);
|
||||
}
|
||||
|
||||
if (font.getAsString(PdfName.BaseFont) != null) {
|
||||
fontNode.put("Name", font.getAsString(PdfName.BaseFont).toString());
|
||||
}
|
||||
|
||||
String subtype = null;
|
||||
if (font.containsKey(PdfName.Subtype)) {
|
||||
subtype = font.getAsName(PdfName.Subtype).toString();
|
||||
uniqueSubtypes.add(subtype); // Add to set to ensure uniqueness
|
||||
}
|
||||
fontNode.put("Subtype", subtype);
|
||||
|
||||
PdfDictionary fontDescriptor = font.getAsDictionary(PdfName.FontDescriptor);
|
||||
if (fontDescriptor != null) {
|
||||
if (fontDescriptor.containsKey(PdfName.ItalicAngle)) {
|
||||
fontNode.put("ItalicAngle", fontDescriptor.getAsNumber(PdfName.ItalicAngle).floatValue());
|
||||
}
|
||||
|
||||
if (fontDescriptor.containsKey(PdfName.Flags)) {
|
||||
int flags = fontDescriptor.getAsNumber(PdfName.Flags).intValue();
|
||||
fontNode.put("IsItalic", (flags & 64) != 0);
|
||||
fontNode.put("IsBold", (flags & 1 << 16) != 0);
|
||||
fontNode.put("IsFixedPitch", (flags & 1) != 0);
|
||||
fontNode.put("IsSerif", (flags & 2) != 0);
|
||||
fontNode.put("IsSymbolic", (flags & 4) != 0);
|
||||
fontNode.put("IsScript", (flags & 8) != 0);
|
||||
fontNode.put("IsNonsymbolic", (flags & 16) != 0);
|
||||
}
|
||||
|
||||
if (fontDescriptor.containsKey(PdfName.FontFamily)) {
|
||||
String fontFamily = fontDescriptor.getAsString(PdfName.FontFamily).toString();
|
||||
fontNode.put("FontFamily", fontFamily);
|
||||
}
|
||||
|
||||
if (fontDescriptor.containsKey(PdfName.FontStretch)) {
|
||||
String fontStretch = fontDescriptor.getAsName(PdfName.FontStretch).toString();
|
||||
fontNode.put("FontStretch", fontStretch);
|
||||
}
|
||||
|
||||
if (fontDescriptor.containsKey(PdfName.FontBBox)) {
|
||||
PdfArray bbox = fontDescriptor.getAsArray(PdfName.FontBBox);
|
||||
fontNode.put("FontBoundingBox", bbox.toString());
|
||||
}
|
||||
|
||||
if (fontDescriptor.containsKey(PdfName.FontWeight)) {
|
||||
float fontWeight = fontDescriptor.getAsNumber(PdfName.FontWeight).floatValue();
|
||||
fontNode.put("FontWeight", fontWeight);
|
||||
}
|
||||
}
|
||||
|
||||
if (font.containsKey(PdfName.ToUnicode)) {
|
||||
fontNode.put("HasToUnicodeMap", true);
|
||||
}
|
||||
|
||||
if (fontNode.size() > 0) {
|
||||
// Create a unique key for this font node based on its attributes
|
||||
String uniqueKey = fontNode.toString();
|
||||
|
||||
// Increment count if this font exists, or initialize it if new
|
||||
if (uniqueFontsMap.containsKey(uniqueKey)) {
|
||||
ObjectNode existingFontNode = uniqueFontsMap.get(uniqueKey);
|
||||
int count = existingFontNode.get("Count").asInt() + 1;
|
||||
existingFontNode.put("Count", count);
|
||||
} else {
|
||||
fontNode.put("Count", 1);
|
||||
uniqueFontsMap.put(uniqueKey, fontNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add unique font entries to fontsArray
|
||||
for (ObjectNode uniqueFontNode : uniqueFontsMap.values()) {
|
||||
fontsArray.add(uniqueFontNode);
|
||||
}
|
||||
|
||||
pageInfo.set("Fonts", fontsArray);
|
||||
|
||||
|
||||
|
||||
|
||||
// Access resources dictionary
|
||||
PdfDictionary resourcesDict = itextDoc.getPage(pageNum).getResources().getPdfObject();
|
||||
|
||||
// Color Spaces & ICC Profiles
|
||||
ArrayNode colorSpacesArray = objectMapper.createArrayNode();
|
||||
PdfDictionary colorSpaces = resourcesDict.getAsDictionary(PdfName.ColorSpace);
|
||||
if (colorSpaces != null) {
|
||||
for (PdfName name : colorSpaces.keySet()) {
|
||||
PdfObject colorSpaceObject = colorSpaces.get(name);
|
||||
if (colorSpaceObject instanceof PdfArray) {
|
||||
PdfArray colorSpaceArray = (PdfArray) colorSpaceObject;
|
||||
if (colorSpaceArray.size() > 1 && colorSpaceArray.get(0) instanceof PdfName && PdfName.ICCBased.equals(colorSpaceArray.get(0))) {
|
||||
ObjectNode iccProfileNode = objectMapper.createObjectNode();
|
||||
PdfStream iccStream = (PdfStream) colorSpaceArray.get(1);
|
||||
byte[] iccData = iccStream.getBytes();
|
||||
// TODO: Further decode and analyze the ICC data if needed
|
||||
iccProfileNode.put("ICC Profile Length", iccData.length);
|
||||
colorSpacesArray.add(iccProfileNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo.set("Color Spaces & ICC Profiles", colorSpacesArray);
|
||||
|
||||
// Other XObjects
|
||||
Map<String, Integer> xObjectCountMap = new HashMap<>(); // To store the count for each type
|
||||
PdfDictionary xObjects = resourcesDict.getAsDictionary(PdfName.XObject);
|
||||
if (xObjects != null) {
|
||||
for (PdfName name : xObjects.keySet()) {
|
||||
PdfStream xObjectStream = xObjects.getAsStream(name);
|
||||
String xObjectType = xObjectStream.getAsName(PdfName.Subtype).toString();
|
||||
|
||||
// Increment the count for this type in the map
|
||||
xObjectCountMap.put(xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the count map to pageInfo (or wherever you want to store it)
|
||||
ObjectNode xObjectCountNode = objectMapper.createObjectNode();
|
||||
for (Map.Entry<String, Integer> entry : xObjectCountMap.entrySet()) {
|
||||
xObjectCountNode.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
pageInfo.set("XObjectCounts", xObjectCountNode);
|
||||
|
||||
|
||||
|
||||
ArrayNode multimediaArray = objectMapper.createArrayNode();
|
||||
for (PdfAnnotation annotation : annotations) {
|
||||
if (PdfName.RichMedia.equals(annotation.getSubtype())) {
|
||||
ObjectNode multimediaNode = objectMapper.createObjectNode();
|
||||
// Extract details from the dictionary as needed
|
||||
multimediaArray.add(multimediaNode);
|
||||
}
|
||||
}
|
||||
pageInfo.set("Multimedia", multimediaArray);
|
||||
|
||||
|
||||
|
||||
pageInfoParent.set("Page " + pageNum, pageInfo);
|
||||
}
|
||||
|
||||
|
||||
jsonOutput.set("BasicInfo", basicInfo);
|
||||
jsonOutput.set("DocumentInfo", docInfoNode);
|
||||
jsonOutput.set("Compliancy", compliancy);
|
||||
jsonOutput.set("Encryption", encryption);
|
||||
jsonOutput.set("Other", other);
|
||||
jsonOutput.set("PerPageInfo", pageInfoParent);
|
||||
|
||||
|
||||
|
||||
// Save JSON to file
|
||||
String jsonString = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonOutput);
|
||||
|
||||
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(jsonString.getBytes(StandardCharsets.UTF_8), "response.json", MediaType.APPLICATION_JSON);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void addOutlinesToArray(PdfOutline outline, ArrayNode arrayNode) {
|
||||
if (outline == null) return;
|
||||
ObjectNode outlineNode = objectMapper.createObjectNode();
|
||||
outlineNode.put("Title", outline.getTitle());
|
||||
// You can add other properties if needed
|
||||
arrayNode.add(outlineNode);
|
||||
|
||||
for (PdfOutline child : outline.getAllChildren()) {
|
||||
addOutlinesToArray(child, arrayNode);
|
||||
}
|
||||
}
|
||||
public String getPageOrientation(double width, double height) {
|
||||
if (width > height) {
|
||||
return "Landscape";
|
||||
} else if (height > width) {
|
||||
return "Portrait";
|
||||
} else {
|
||||
return "Square";
|
||||
}
|
||||
}
|
||||
public String getPageSize(double width, double height) {
|
||||
// Common aspect ratios used for standard paper sizes
|
||||
double[] aspectRatios = {4.0 / 3.0, 3.0 / 2.0, Math.sqrt(2.0), 16.0 / 9.0};
|
||||
|
||||
// Check if the page matches any common aspect ratio
|
||||
for (double aspectRatio : aspectRatios) {
|
||||
if (isCloseToAspectRatio(width, height, aspectRatio)) {
|
||||
return "Standard";
|
||||
}
|
||||
}
|
||||
|
||||
// If not a standard aspect ratio, consider it as a custom size
|
||||
return "Custom";
|
||||
}
|
||||
private boolean isCloseToAspectRatio(double width, double height, double aspectRatio) {
|
||||
// Calculate the aspect ratio of the page
|
||||
double pageAspectRatio = width / height;
|
||||
|
||||
// Compare the page aspect ratio with the common aspect ratio within a threshold
|
||||
return Math.abs(pageAspectRatio - aspectRatio) <= 0.05;
|
||||
}
|
||||
|
||||
public boolean checkForStandard(PdfDocument document, String standardKeyword) {
|
||||
// Check Output Intents
|
||||
boolean foundInOutputIntents = checkOutputIntent(document, standardKeyword);
|
||||
if (foundInOutputIntents) return true;
|
||||
|
||||
// Check XMP Metadata (rudimentary)
|
||||
try {
|
||||
byte[] metadataBytes = document.getXmpMetadata();
|
||||
if (metadataBytes != null) {
|
||||
XMPMeta xmpMeta = XMPMetaFactory.parseFromBuffer(metadataBytes);
|
||||
String xmpString = xmpMeta.dumpObject();
|
||||
if (xmpString.contains(standardKeyword)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (XMPException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public boolean checkOutputIntent(PdfDocument document, String standard) {
|
||||
PdfArray outputIntents = document.getCatalog().getPdfObject().getAsArray(PdfName.OutputIntents);
|
||||
if (outputIntents != null && !outputIntents.isEmpty()) {
|
||||
for (int i = 0; i < outputIntents.size(); i++) {
|
||||
PdfDictionary outputIntentDict = outputIntents.getAsDictionary(i);
|
||||
if (outputIntentDict != null) {
|
||||
PdfString s = outputIntentDict.getAsString(PdfName.S);
|
||||
if (s != null && s.toString().contains(standard)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public ArrayNode exploreStructureTree(List<Object> nodes) {
|
||||
ArrayNode elementsArray = objectMapper.createArrayNode();
|
||||
if (nodes != null) {
|
||||
for (Object obj : nodes) {
|
||||
if (obj instanceof PDStructureNode) {
|
||||
PDStructureNode node = (PDStructureNode) obj;
|
||||
ObjectNode elementNode = objectMapper.createObjectNode();
|
||||
|
||||
if (node instanceof PDStructureElement) {
|
||||
PDStructureElement structureElement = (PDStructureElement) node;
|
||||
elementNode.put("Type", structureElement.getStructureType());
|
||||
elementNode.put("Content", getContent(structureElement));
|
||||
|
||||
// Recursively explore child elements
|
||||
ArrayNode childElements = exploreStructureTree(structureElement.getKids());
|
||||
if (childElements.size() > 0) {
|
||||
elementNode.set("Children", childElements);
|
||||
}
|
||||
}
|
||||
elementsArray.add(elementNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
return elementsArray;
|
||||
}
|
||||
|
||||
|
||||
public String getContent(PDStructureElement structureElement) {
|
||||
StringBuilder contentBuilder = new StringBuilder();
|
||||
|
||||
for (Object item : structureElement.getKids()) {
|
||||
if (item instanceof COSString) {
|
||||
COSString cosString = (COSString) item;
|
||||
contentBuilder.append(cosString.getString());
|
||||
} else if (item instanceof PDStructureElement) {
|
||||
// For simplicity, we're handling only COSString and PDStructureElement here
|
||||
// but a more comprehensive method would handle other types too
|
||||
contentBuilder.append(getContent((PDStructureElement) item));
|
||||
}
|
||||
}
|
||||
|
||||
return contentBuilder.toString();
|
||||
}
|
||||
|
||||
|
||||
private String formatDate(Calendar calendar) {
|
||||
if (calendar != null) {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
return sdf.format(calendar.getTime());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String getPageModeDescription(PdfName pageMode) {
|
||||
return pageMode != null ? pageMode.toString().replaceFirst("/", "") : "Unknown";
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,10 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@RestController
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
public class PasswordController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PasswordController.class);
|
||||
@@ -27,7 +29,7 @@ public class PasswordController {
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-password")
|
||||
@Operation(
|
||||
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."
|
||||
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(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@@ -44,43 +46,43 @@ public class PasswordController {
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/add-password")
|
||||
@Operation(
|
||||
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."
|
||||
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(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@Parameter(description = "The input PDF file to which the password should be added", required = true)
|
||||
MultipartFile fileInput,
|
||||
@RequestParam(defaultValue = "", name = "ownerPassword")
|
||||
@RequestParam(value = "", name = "ownerPassword", required = false, defaultValue = "")
|
||||
@Parameter(description = "The owner password to be added to the PDF file (Restricts what can be done with the document once it is opened)")
|
||||
String ownerPassword,
|
||||
@RequestParam(defaultValue = "", name = "password")
|
||||
@RequestParam( name = "password", required = false, defaultValue = "")
|
||||
@Parameter(description = "The password to be added to the PDF file (Restricts the opening of the document itself.)")
|
||||
String password,
|
||||
@RequestParam(defaultValue = "128", name = "keyLength")
|
||||
@RequestParam( name = "keyLength", required = false, defaultValue = "256")
|
||||
@Parameter(description = "The length of the encryption key", schema = @Schema(allowableValues = {"40", "128", "256"}))
|
||||
int keyLength,
|
||||
@RequestParam(defaultValue = "false", name = "canAssembleDocument")
|
||||
@RequestParam( name = "canAssembleDocument", required = false)
|
||||
@Parameter(description = "Whether the document assembly is allowed", example = "false")
|
||||
boolean canAssembleDocument,
|
||||
@RequestParam(defaultValue = "false", name = "canExtractContent")
|
||||
@RequestParam( name = "canExtractContent", required = false)
|
||||
@Parameter(description = "Whether content extraction for accessibility is allowed", example = "false")
|
||||
boolean canExtractContent,
|
||||
@RequestParam(defaultValue = "false", name = "canExtractForAccessibility")
|
||||
@RequestParam( name = "canExtractForAccessibility", required = false)
|
||||
@Parameter(description = "Whether content extraction for accessibility is allowed", example = "false")
|
||||
boolean canExtractForAccessibility,
|
||||
@RequestParam(defaultValue = "false", name = "canFillInForm")
|
||||
@RequestParam( name = "canFillInForm", required = false)
|
||||
@Parameter(description = "Whether form filling is allowed", example = "false")
|
||||
boolean canFillInForm,
|
||||
@RequestParam(defaultValue = "false", name = "canModify")
|
||||
@RequestParam( name = "canModify", required = false)
|
||||
@Parameter(description = "Whether the document modification is allowed", example = "false")
|
||||
boolean canModify,
|
||||
@RequestParam(defaultValue = "false", name = "canModifyAnnotations")
|
||||
@RequestParam( name = "canModifyAnnotations", required = false)
|
||||
@Parameter(description = "Whether modification of annotations is allowed", example = "false")
|
||||
boolean canModifyAnnotations,
|
||||
@RequestParam(defaultValue = "false", name = "canPrint")
|
||||
@RequestParam(name = "canPrint", required = false)
|
||||
@Parameter(description = "Whether printing of the document is allowed", example = "false")
|
||||
boolean canPrint,
|
||||
@RequestParam(defaultValue = "false", name = "canPrintFaithful")
|
||||
@RequestParam( name = "canPrintFaithful", required = false)
|
||||
@Parameter(description = "Whether faithful printing is allowed", example = "false")
|
||||
boolean canPrintFaithful
|
||||
) throws IOException {
|
||||
@@ -96,15 +98,15 @@ public class PasswordController {
|
||||
ap.setCanPrint(!canPrint);
|
||||
ap.setCanPrintFaithful(!canPrintFaithful);
|
||||
StandardProtectionPolicy spp = new StandardProtectionPolicy(ownerPassword, password, ap);
|
||||
|
||||
|
||||
|
||||
spp.setEncryptionKeyLength(keyLength);
|
||||
|
||||
if(!"".equals(ownerPassword) || !"".equals(password)) {
|
||||
spp.setEncryptionKeyLength(keyLength);
|
||||
}
|
||||
spp.setPermissions(ap);
|
||||
|
||||
document.protect(spp);
|
||||
|
||||
if("".equals(ownerPassword) && "".equals(password))
|
||||
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_permissions.pdf");
|
||||
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package stirling.software.SPDF.controller.api.security;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
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.image.LosslessFactory;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
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.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.model.PDFText;
|
||||
import stirling.software.SPDF.pdf.TextFinder;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@RestController
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
public class RedactController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RedactController.class);
|
||||
|
||||
|
||||
@PostMapping(value = "/auto-redact", consumes = "multipart/form-data")
|
||||
@Operation(summary = "Redacts listOfText in a PDF document",
|
||||
description = "This operation takes an input PDF file and redacts the provided listOfText. Input:PDF, Output:PDF, Type:SISO")
|
||||
public ResponseEntity<byte[]> redactPdf(
|
||||
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
|
||||
@Parameter(description = "List of listOfText to redact from the PDF", required = true, schema = @Schema(type = "string")) @RequestParam("listOfText") String listOfTextString,
|
||||
@RequestParam(value = "useRegex", required = false) boolean useRegex,
|
||||
@RequestParam(value = "wholeWordSearch", required = false) boolean wholeWordSearchBool,
|
||||
@RequestParam(value = "customPadding", required = false) float customPadding,
|
||||
@RequestParam(value = "convertPDFToImage", required = false) boolean convertPDFToImage) throws Exception {
|
||||
|
||||
System.out.println(listOfTextString);
|
||||
String[] listOfText = listOfTextString.split("\n");
|
||||
byte[] bytes = file.getBytes();
|
||||
PDDocument document = PDDocument.load(new ByteArrayInputStream(bytes));
|
||||
for (String text : listOfText) {
|
||||
text = text.trim();
|
||||
System.out.println(text);
|
||||
TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool);
|
||||
List<PDFText> foundTexts = textFinder.getTextLocations(document);
|
||||
redactFoundText(document, foundTexts, customPadding);
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (convertPDFToImage) {
|
||||
PDDocument imageDocument = new PDDocument();
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
for (int page = 0; page < document.getNumberOfPages(); ++page) {
|
||||
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
|
||||
PDPage newPage = new PDPage(new PDRectangle(bim.getWidth(), bim.getHeight()));
|
||||
imageDocument.addPage(newPage);
|
||||
PDImageXObject pdImage = LosslessFactory.createFromImage(imageDocument, bim);
|
||||
PDPageContentStream contentStream = new PDPageContentStream(imageDocument, newPage);
|
||||
contentStream.drawImage(pdImage, 0, 0);
|
||||
contentStream.close();
|
||||
}
|
||||
document.close();
|
||||
document = imageDocument;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
document.save(baos);
|
||||
document.close();
|
||||
|
||||
byte[] pdfContent = baos.toByteArray();
|
||||
return WebResponseUtils.bytesToWebResponse(pdfContent,
|
||||
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_redacted.pdf");
|
||||
}
|
||||
|
||||
|
||||
private void redactFoundText(PDDocument document, List<PDFText> blocks, float customPadding) throws IOException {
|
||||
var allPages = document.getDocumentCatalog().getPages();
|
||||
|
||||
for (PDFText block : blocks) {
|
||||
var page = allPages.get(block.getPageIndex());
|
||||
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true, true);
|
||||
contentStream.setNonStrokingColor(Color.BLACK);
|
||||
float padding = (block.getY2() - block.getY1()) * 0.3f + customPadding;
|
||||
PDRectangle pageBox = page.getBBox();
|
||||
contentStream.addRect(block.getX1(), pageBox.getHeight() - block.getY1() - padding, block.getX2() - block.getX1(), block.getY2() - block.getY1() + 2 * padding);
|
||||
contentStream.fill();
|
||||
contentStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package stirling.software.SPDF.controller.api.security;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.pdfbox.cos.COSDictionary;
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageTree;
|
||||
import org.apache.pdfbox.pdmodel.PDResources;
|
||||
import org.apache.pdfbox.pdmodel.common.PDMetadata;
|
||||
import org.apache.pdfbox.pdmodel.interactive.action.PDAction;
|
||||
import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript;
|
||||
import org.apache.pdfbox.pdmodel.interactive.action.PDActionLaunch;
|
||||
import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI;
|
||||
import org.apache.pdfbox.pdmodel.interactive.action.PDFormFieldAdditionalActions;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
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.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
public class SanitizeController {
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf")
|
||||
@Operation(summary = "Sanitize a PDF file",
|
||||
description = "This endpoint processes a PDF file and removes specific elements based on the provided options. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> sanitizePDF(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@Parameter(description = "The input PDF file to be sanitized")
|
||||
MultipartFile inputFile,
|
||||
@RequestParam(name = "removeJavaScript", required = false, defaultValue = "false")
|
||||
@Parameter(description = "Remove JavaScript actions from the PDF if set to true")
|
||||
Boolean removeJavaScript,
|
||||
@RequestParam(name = "removeEmbeddedFiles", required = false, defaultValue = "false")
|
||||
@Parameter(description = "Remove embedded files from the PDF if set to true")
|
||||
Boolean removeEmbeddedFiles,
|
||||
@RequestParam(name = "removeMetadata", required = false, defaultValue = "false")
|
||||
@Parameter(description = "Remove metadata from the PDF if set to true")
|
||||
Boolean removeMetadata,
|
||||
@RequestParam(name = "removeLinks", required = false, defaultValue = "false")
|
||||
@Parameter(description = "Remove links from the PDF if set to true")
|
||||
Boolean removeLinks,
|
||||
@RequestParam(name = "removeFonts", required = false, defaultValue = "false")
|
||||
@Parameter(description = "Remove fonts from the PDF if set to true")
|
||||
Boolean removeFonts) throws IOException {
|
||||
|
||||
try (PDDocument document = PDDocument.load(inputFile.getInputStream())) {
|
||||
if (removeJavaScript) {
|
||||
sanitizeJavaScript(document);
|
||||
}
|
||||
|
||||
if (removeEmbeddedFiles) {
|
||||
sanitizeEmbeddedFiles(document);
|
||||
}
|
||||
|
||||
if (removeMetadata) {
|
||||
sanitizeMetadata(document);
|
||||
}
|
||||
|
||||
if (removeLinks) {
|
||||
sanitizeLinks(document);
|
||||
}
|
||||
|
||||
if (removeFonts) {
|
||||
sanitizeFonts(document);
|
||||
}
|
||||
|
||||
return WebResponseUtils.pdfDocToWebResponse(document, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_sanitized.pdf");
|
||||
}
|
||||
}
|
||||
private void sanitizeJavaScript(PDDocument document) throws IOException {
|
||||
// Get the root dictionary (catalog) of the PDF
|
||||
PDDocumentCatalog catalog = document.getDocumentCatalog();
|
||||
|
||||
// Get the Names dictionary
|
||||
COSDictionary namesDict = (COSDictionary) catalog.getCOSObject().getDictionaryObject(COSName.NAMES);
|
||||
|
||||
if (namesDict != null) {
|
||||
// Get the JavaScript dictionary
|
||||
COSDictionary javaScriptDict = (COSDictionary) namesDict.getDictionaryObject(COSName.getPDFName("JavaScript"));
|
||||
|
||||
if (javaScriptDict != null) {
|
||||
// Remove the JavaScript dictionary
|
||||
namesDict.removeItem(COSName.getPDFName("JavaScript"));
|
||||
}
|
||||
}
|
||||
|
||||
for (PDPage page : document.getPages()) {
|
||||
for (PDAnnotation annotation : page.getAnnotations()) {
|
||||
if (annotation instanceof PDAnnotationWidget) {
|
||||
PDAnnotationWidget widget = (PDAnnotationWidget) annotation;
|
||||
PDAction action = widget.getAction();
|
||||
if (action instanceof PDActionJavaScript) {
|
||||
widget.setAction(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
|
||||
if (acroForm != null) {
|
||||
for (PDField field : acroForm.getFields()) {
|
||||
PDFormFieldAdditionalActions actions = field.getActions();
|
||||
if(actions != null) {
|
||||
if (actions.getC() instanceof PDActionJavaScript) {
|
||||
actions.setC(null);
|
||||
}
|
||||
if (actions.getF() instanceof PDActionJavaScript) {
|
||||
actions.setF(null);
|
||||
}
|
||||
if (actions.getK() instanceof PDActionJavaScript) {
|
||||
actions.setK(null);
|
||||
}
|
||||
if (actions.getV() instanceof PDActionJavaScript) {
|
||||
actions.setV(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private void sanitizeEmbeddedFiles(PDDocument document) {
|
||||
PDPageTree allPages = document.getPages();
|
||||
|
||||
for (PDPage page : allPages) {
|
||||
PDResources res = page.getResources();
|
||||
|
||||
// Remove embedded files from the PDF
|
||||
res.getCOSObject().removeItem(COSName.getPDFName("EmbeddedFiles"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void sanitizeMetadata(PDDocument document) {
|
||||
PDMetadata metadata = document.getDocumentCatalog().getMetadata();
|
||||
if (metadata != null) {
|
||||
document.getDocumentCatalog().setMetadata(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void sanitizeLinks(PDDocument document) throws IOException {
|
||||
for (PDPage page : document.getPages()) {
|
||||
for (PDAnnotation annotation : page.getAnnotations()) {
|
||||
if (annotation instanceof PDAnnotationLink) {
|
||||
PDAction action = ((PDAnnotationLink) annotation).getAction();
|
||||
if (action instanceof PDActionLaunch || action instanceof PDActionURI) {
|
||||
((PDAnnotationLink) annotation).setAction(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sanitizeFonts(PDDocument document) {
|
||||
for (PDPage page : document.getPages()) {
|
||||
page.getResources().getCOSObject().removeItem(COSName.getPDFName("Font"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +1,26 @@
|
||||
package stirling.software.SPDF.controller.api.security;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.font.PDFont;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType0Font;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
|
||||
import org.apache.pdfbox.util.Matrix;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
@@ -19,79 +30,167 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
public class WatermarkController {
|
||||
|
||||
@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 text, font size, rotation, opacity, width spacer, and height spacer.")
|
||||
public ResponseEntity<byte[]> addWatermark(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@Parameter(description = "The input PDF file to add a watermark")
|
||||
MultipartFile pdfFile,
|
||||
@RequestParam("watermarkText")
|
||||
@Parameter(description = "The watermark text to add to the PDF file")
|
||||
String watermarkText,
|
||||
@RequestParam(defaultValue = "30", name = "fontSize")
|
||||
@Parameter(description = "The font size of the watermark text", example = "30")
|
||||
float fontSize,
|
||||
@RequestParam(defaultValue = "0", name = "rotation")
|
||||
@Parameter(description = "The rotation of the watermark text in degrees", example = "0")
|
||||
float rotation,
|
||||
@RequestParam(defaultValue = "0.5", name = "opacity")
|
||||
@Parameter(description = "The opacity of the watermark text (0.0 - 1.0)", example = "0.5")
|
||||
float opacity,
|
||||
@RequestParam(defaultValue = "50", name = "widthSpacer")
|
||||
@Parameter(description = "The width spacer between watermark texts", example = "50")
|
||||
int widthSpacer,
|
||||
@RequestParam(defaultValue = "50", name = "heightSpacer")
|
||||
@Parameter(description = "The height spacer between watermark texts", example = "50")
|
||||
int heightSpacer) throws IOException {
|
||||
@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")
|
||||
public ResponseEntity<byte[]> addWatermark(
|
||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to add a watermark") MultipartFile pdfFile,
|
||||
@RequestParam(required = true) @Parameter(description = "The watermark type (text or image)") String watermarkType,
|
||||
@RequestParam(required = false) @Parameter(description = "The watermark text") String watermarkText,
|
||||
@RequestPart(required = false) @Parameter(description = "The watermark image") MultipartFile watermarkImage,
|
||||
|
||||
@RequestParam(defaultValue = "roman", name = "alphabet") @Parameter(description = "The selected alphabet",
|
||||
schema = @Schema(type = "string",
|
||||
allowableValues = {"roman","arabic","japanese","korean","chinese"},
|
||||
defaultValue = "roman")) String alphabet,
|
||||
@RequestParam(defaultValue = "30", name = "fontSize") @Parameter(description = "The font size of the watermark text", example = "30") float fontSize,
|
||||
@RequestParam(defaultValue = "0", name = "rotation") @Parameter(description = "The rotation of the watermark in degrees", example = "0") float rotation,
|
||||
@RequestParam(defaultValue = "0.5", name = "opacity") @Parameter(description = "The opacity of the watermark (0.0 - 1.0)", example = "0.5") float opacity,
|
||||
@RequestParam(defaultValue = "50", name = "widthSpacer") @Parameter(description = "The width spacer between watermark elements", example = "50") int widthSpacer,
|
||||
@RequestParam(defaultValue = "50", name = "heightSpacer") @Parameter(description = "The height spacer between watermark elements", example = "50") int heightSpacer)
|
||||
throws IOException, Exception {
|
||||
|
||||
// Load the input PDF
|
||||
PDDocument document = PDDocument.load(pdfFile.getInputStream());
|
||||
// Load the input PDF
|
||||
PDDocument document = PDDocument.load(pdfFile.getInputStream());
|
||||
|
||||
// Create a page in the document
|
||||
for (PDPage page : document.getPages()) {
|
||||
// Create a page in the document
|
||||
for (PDPage page : document.getPages()) {
|
||||
|
||||
// Get the page's content stream
|
||||
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true);
|
||||
// Get the page's content stream
|
||||
PDPageContentStream contentStream = new PDPageContentStream(document, page,
|
||||
PDPageContentStream.AppendMode.APPEND, true);
|
||||
|
||||
// Set transparency
|
||||
PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
|
||||
graphicsState.setNonStrokingAlphaConstant(opacity);
|
||||
contentStream.setGraphicsStateParameters(graphicsState);
|
||||
// Set transparency
|
||||
PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
|
||||
graphicsState.setNonStrokingAlphaConstant(opacity);
|
||||
contentStream.setGraphicsStateParameters(graphicsState);
|
||||
|
||||
// Set font of watermark
|
||||
PDFont font = PDType1Font.HELVETICA_BOLD;
|
||||
contentStream.beginText();
|
||||
contentStream.setFont(font, fontSize);
|
||||
contentStream.setNonStrokingColor(Color.LIGHT_GRAY);
|
||||
if (watermarkType.equalsIgnoreCase("text")) {
|
||||
addTextWatermark(contentStream, watermarkText, document, page, rotation, widthSpacer, heightSpacer,
|
||||
fontSize, alphabet);
|
||||
} else if (watermarkType.equalsIgnoreCase("image")) {
|
||||
addImageWatermark(contentStream, watermarkImage, document, page, rotation, widthSpacer, heightSpacer,
|
||||
fontSize);
|
||||
}
|
||||
|
||||
// Set size and location of watermark
|
||||
float pageWidth = page.getMediaBox().getWidth();
|
||||
float pageHeight = page.getMediaBox().getHeight();
|
||||
float watermarkWidth = widthSpacer + font.getStringWidth(watermarkText) * fontSize / 1000;
|
||||
float watermarkHeight = heightSpacer + fontSize;
|
||||
int watermarkRows = (int) (pageHeight / watermarkHeight + 1);
|
||||
int watermarkCols = (int) (pageWidth / watermarkWidth + 1);
|
||||
// Close the content stream
|
||||
contentStream.close();
|
||||
}
|
||||
|
||||
// Add the watermark text
|
||||
for (int i = 0; i < watermarkRows; i++) {
|
||||
for (int j = 0; j < watermarkCols; j++) {
|
||||
contentStream.setTextMatrix(Matrix.getRotateInstance((float) Math.toRadians(rotation), j * watermarkWidth, i * watermarkHeight));
|
||||
contentStream.showTextWithPositioning(new Object[] { watermarkText });
|
||||
}
|
||||
return WebResponseUtils.pdfDocToWebResponse(document,
|
||||
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf");
|
||||
}
|
||||
|
||||
private void addTextWatermark(PDPageContentStream contentStream, String watermarkText, PDDocument document,
|
||||
PDPage page, float rotation, int widthSpacer, int heightSpacer, float fontSize, String alphabet) throws IOException {
|
||||
String resourceDir = "";
|
||||
PDFont font = PDType1Font.HELVETICA_BOLD;
|
||||
switch (alphabet) {
|
||||
case "arabic":
|
||||
resourceDir = "static/fonts/NotoSansArabic-Regular.ttf";
|
||||
break;
|
||||
case "japanese":
|
||||
resourceDir = "static/fonts/Meiryo.ttf";
|
||||
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("")) {
|
||||
ClassPathResource classPathResource = new ClassPathResource(resourceDir);
|
||||
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
|
||||
File tempFile = File.createTempFile("NotoSansFont", fileExtension);
|
||||
try (InputStream is = classPathResource.getInputStream(); FileOutputStream os = new FileOutputStream(tempFile)) {
|
||||
IOUtils.copy(is, os);
|
||||
}
|
||||
|
||||
contentStream.endText();
|
||||
|
||||
// Close the content stream
|
||||
contentStream.close();
|
||||
|
||||
font = PDType0Font.load(document, tempFile);
|
||||
tempFile.deleteOnExit();
|
||||
}
|
||||
return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf");
|
||||
}
|
||||
|
||||
contentStream.setFont(font, fontSize);
|
||||
contentStream.setNonStrokingColor(Color.LIGHT_GRAY);
|
||||
|
||||
// Set size and location of text watermark
|
||||
float watermarkWidth = widthSpacer + font.getStringWidth(watermarkText) * fontSize / 1000;
|
||||
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
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addImageWatermark(PDPageContentStream contentStream, MultipartFile watermarkImage, PDDocument document, PDPage page, float rotation,
|
||||
int widthSpacer, int heightSpacer, float fontSize) throws IOException {
|
||||
|
||||
// Load the watermark image
|
||||
BufferedImage image = ImageIO.read(watermarkImage.getInputStream());
|
||||
|
||||
// Compute width based on original aspect ratio
|
||||
float aspectRatio = (float) image.getWidth() / (float) image.getHeight();
|
||||
|
||||
// Desired physical height (in PDF points)
|
||||
float desiredPhysicalHeight = fontSize ;
|
||||
|
||||
// Desired physical width based on the aspect ratio
|
||||
float desiredPhysicalWidth = desiredPhysicalHeight * aspectRatio;
|
||||
|
||||
// Convert the BufferedImage to PDImageXObject
|
||||
PDImageXObject xobject = LosslessFactory.createFromImage(document, image);
|
||||
|
||||
// Calculate the number of rows and columns for watermarks
|
||||
float pageWidth = page.getMediaBox().getWidth();
|
||||
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++) {
|
||||
for (int j = 0; j < watermarkCols; j++) {
|
||||
float x = j * (desiredPhysicalWidth + widthSpacer);
|
||||
float y = i * (desiredPhysicalHeight + heightSpacer);
|
||||
|
||||
// Save the graphics state
|
||||
contentStream.saveGraphicsState();
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
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.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.io.support.ResourcePatternUtils;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import stirling.software.SPDF.config.security.UserService;
|
||||
import stirling.software.SPDF.model.User;
|
||||
import stirling.software.SPDF.repository.UserRepository;
|
||||
@Controller
|
||||
@Tag(name = "Account Security", description = "Account Security APIs")
|
||||
public class AccountWebController {
|
||||
|
||||
|
||||
@GetMapping("/login")
|
||||
public String login(HttpServletRequest request, Model model, Authentication authentication) {
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
if (request.getParameter("error") != null) {
|
||||
|
||||
model.addAttribute("error", request.getParameter("error"));
|
||||
}
|
||||
if (request.getParameter("logout") != null) {
|
||||
|
||||
model.addAttribute("logoutMessage", "You have been logged out.");
|
||||
}
|
||||
|
||||
return "login";
|
||||
}
|
||||
@Autowired
|
||||
private UserRepository userRepository; // Assuming you have a repository for user operations
|
||||
|
||||
@Autowired
|
||||
private UserService userService; // Assuming you have a repository for user operations
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@GetMapping("/addUsers")
|
||||
public String showAddUserForm(Model model) {
|
||||
List<User> allUsers = userRepository.findAll();
|
||||
model.addAttribute("users", allUsers);
|
||||
return "addUsers";
|
||||
}
|
||||
|
||||
|
||||
|
||||
@GetMapping("/account")
|
||||
public String account(HttpServletRequest request, Model model, Authentication authentication) {
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
return "redirect:/";
|
||||
}
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
Object principal = authentication.getPrincipal();
|
||||
|
||||
if (principal instanceof UserDetails) {
|
||||
// Cast the principal object to UserDetails
|
||||
UserDetails userDetails = (UserDetails) principal;
|
||||
|
||||
// Retrieve username and other attributes
|
||||
String username = userDetails.getUsername();
|
||||
|
||||
// Fetch user details from the database
|
||||
Optional<User> user = userRepository.findByUsername(username); // Assuming findByUsername method exists
|
||||
if (!user.isPresent()) {
|
||||
// Handle error appropriately
|
||||
return "redirect:/error"; // Example redirection in case of error
|
||||
}
|
||||
|
||||
// Convert settings map to JSON string
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
String settingsJson;
|
||||
try {
|
||||
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
||||
} catch (JsonProcessingException e) {
|
||||
// Handle JSON conversion error
|
||||
e.printStackTrace();
|
||||
return "redirect:/error"; // Example redirection in case of error
|
||||
}
|
||||
|
||||
// Add attributes to the model
|
||||
model.addAttribute("username", username);
|
||||
model.addAttribute("role", user.get().getRolesAsString());
|
||||
model.addAttribute("settings", settingsJson);
|
||||
}
|
||||
} else {
|
||||
return "redirect:/";
|
||||
}
|
||||
return "account";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,86 +1,109 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
@Controller
|
||||
public class ConverterWebController {
|
||||
|
||||
@GetMapping("/img-to-pdf")
|
||||
@Hidden
|
||||
public String convertImgToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "img-to-pdf");
|
||||
return "convert/img-to-pdf";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/pdf-to-img")
|
||||
@Hidden
|
||||
public String pdfToimgForm(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-to-img");
|
||||
return "convert/pdf-to-img";
|
||||
}
|
||||
|
||||
@GetMapping("/file-to-pdf")
|
||||
@Hidden
|
||||
public String convertToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "file-to-pdf");
|
||||
return "convert/file-to-pdf";
|
||||
}
|
||||
|
||||
|
||||
|
||||
//PDF TO......
|
||||
|
||||
@GetMapping("/pdf-to-html")
|
||||
@Hidden
|
||||
public ModelAndView pdfToHTML() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-html");
|
||||
modelAndView.addObject("currentPage", "pdf-to-html");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-presentation")
|
||||
@Hidden
|
||||
public ModelAndView pdfToPresentation() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-presentation");
|
||||
modelAndView.addObject("currentPage", "pdf-to-presentation");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-text")
|
||||
@Hidden
|
||||
public ModelAndView pdfToText() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-text");
|
||||
modelAndView.addObject("currentPage", "pdf-to-text");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-word")
|
||||
@Hidden
|
||||
public ModelAndView pdfToWord() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-word");
|
||||
modelAndView.addObject("currentPage", "pdf-to-word");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-xml")
|
||||
@Hidden
|
||||
public ModelAndView pdfToXML() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-xml");
|
||||
modelAndView.addObject("currentPage", "pdf-to-xml");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/pdf-to-pdfa")
|
||||
@Hidden
|
||||
public String pdfToPdfAForm(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-to-pdfa");
|
||||
return "convert/pdf-to-pdfa";
|
||||
}
|
||||
}
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@Controller
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
public class ConverterWebController {
|
||||
|
||||
@GetMapping("/img-to-pdf")
|
||||
@Hidden
|
||||
public String convertImgToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "img-to-pdf");
|
||||
return "convert/img-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/html-to-pdf")
|
||||
@Hidden
|
||||
public String convertHTMLToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "html-to-pdf");
|
||||
return "convert/html-to-pdf";
|
||||
}
|
||||
@GetMapping("/markdown-to-pdf")
|
||||
@Hidden
|
||||
public String convertMarkdownToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "markdown-to-pdf");
|
||||
return "convert/markdown-to-pdf";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/url-to-pdf")
|
||||
@Hidden
|
||||
public String convertURLToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "url-to-pdf");
|
||||
return "convert/url-to-pdf";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/pdf-to-img")
|
||||
@Hidden
|
||||
public String pdfToimgForm(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-to-img");
|
||||
return "convert/pdf-to-img";
|
||||
}
|
||||
|
||||
@GetMapping("/file-to-pdf")
|
||||
@Hidden
|
||||
public String convertToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "file-to-pdf");
|
||||
return "convert/file-to-pdf";
|
||||
}
|
||||
|
||||
|
||||
|
||||
//PDF TO......
|
||||
|
||||
@GetMapping("/pdf-to-html")
|
||||
@Hidden
|
||||
public ModelAndView pdfToHTML() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-html");
|
||||
modelAndView.addObject("currentPage", "pdf-to-html");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-presentation")
|
||||
@Hidden
|
||||
public ModelAndView pdfToPresentation() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-presentation");
|
||||
modelAndView.addObject("currentPage", "pdf-to-presentation");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-text")
|
||||
@Hidden
|
||||
public ModelAndView pdfToText() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-text");
|
||||
modelAndView.addObject("currentPage", "pdf-to-text");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-word")
|
||||
@Hidden
|
||||
public ModelAndView pdfToWord() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-word");
|
||||
modelAndView.addObject("currentPage", "pdf-to-word");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-xml")
|
||||
@Hidden
|
||||
public ModelAndView pdfToXML() {
|
||||
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-xml");
|
||||
modelAndView.addObject("currentPage", "pdf-to-xml");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/pdf-to-pdfa")
|
||||
@Hidden
|
||||
public String pdfToPdfAForm(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-to-pdfa");
|
||||
return "convert/pdf-to-pdfa";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,83 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
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.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.io.support.ResourcePatternUtils;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@Controller
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class GeneralWebController {
|
||||
|
||||
|
||||
|
||||
|
||||
@GetMapping("/pipeline")
|
||||
@Hidden
|
||||
public String pipelineForm(Model model) {
|
||||
model.addAttribute("currentPage", "pipeline");
|
||||
|
||||
List<String> pipelineConfigs = new ArrayList<>();
|
||||
try (Stream<Path> paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) {
|
||||
List<Path> jsonFiles = paths
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(p -> p.toString().endsWith(".json"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (Path jsonFile : jsonFiles) {
|
||||
String content = Files.readString(jsonFile, StandardCharsets.UTF_8);
|
||||
pipelineConfigs.add(content);
|
||||
}
|
||||
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
|
||||
for (String config : pipelineConfigs) {
|
||||
Map<String, Object> jsonContent = new ObjectMapper().readValue(config, Map.class);
|
||||
String name = (String) jsonContent.get("name");
|
||||
Map<String, String> configWithName = new HashMap<>();
|
||||
configWithName.put("json", config);
|
||||
configWithName.put("name", name);
|
||||
pipelineConfigsWithNames.add(configWithName);
|
||||
}
|
||||
model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames);
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
model.addAttribute("pipelineConfigs", pipelineConfigs);
|
||||
|
||||
return "pipeline";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@GetMapping("/merge-pdfs")
|
||||
@Hidden
|
||||
public String mergePdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "merge-pdfs");
|
||||
return "merge-pdfs";
|
||||
}
|
||||
@GetMapping("/about")
|
||||
@Hidden
|
||||
public String gameForm(Model model) {
|
||||
model.addAttribute("currentPage", "about");
|
||||
return "about";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/multi-tool")
|
||||
@Hidden
|
||||
@@ -29,17 +85,7 @@ public class GeneralWebController {
|
||||
model.addAttribute("currentPage", "multi-tool");
|
||||
return "multi-tool";
|
||||
}
|
||||
|
||||
@GetMapping("/")
|
||||
public String home(Model model) {
|
||||
model.addAttribute("currentPage", "home");
|
||||
return "home";
|
||||
}
|
||||
|
||||
@GetMapping("/home")
|
||||
public String root(Model model) {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/remove-pages")
|
||||
@Hidden
|
||||
@@ -55,6 +101,20 @@ public class GeneralWebController {
|
||||
return "pdf-organizer";
|
||||
}
|
||||
|
||||
@GetMapping("/extract-page")
|
||||
@Hidden
|
||||
public String extractPages(Model model) {
|
||||
model.addAttribute("currentPage", "extract-page");
|
||||
return "extract-page";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-single-page")
|
||||
@Hidden
|
||||
public String pdfToSinglePage(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-to-single-page");
|
||||
return "pdf-to-single-page";
|
||||
}
|
||||
|
||||
@GetMapping("/rotate-pdf")
|
||||
@Hidden
|
||||
public String rotatePdfForm(Model model) {
|
||||
@@ -73,23 +133,47 @@ public class GeneralWebController {
|
||||
@Hidden
|
||||
public String signForm(Model model) {
|
||||
model.addAttribute("currentPage", "sign");
|
||||
model.addAttribute("fonts", getFontNames());
|
||||
return "sign";
|
||||
}
|
||||
|
||||
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@ResponseBody
|
||||
@Hidden
|
||||
public String getRobotsTxt() {
|
||||
String allowGoogleVisibility = System.getProperty("ALLOW_GOOGLE_VISIBILITY");
|
||||
if (allowGoogleVisibility == null)
|
||||
allowGoogleVisibility = System.getenv("ALLOW_GOOGLE_VISIBILITY");
|
||||
if (allowGoogleVisibility == null)
|
||||
allowGoogleVisibility = "false";
|
||||
if (Boolean.parseBoolean(allowGoogleVisibility)) {
|
||||
return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /";
|
||||
} else {
|
||||
return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /";
|
||||
|
||||
@Autowired
|
||||
private ResourceLoader resourceLoader;
|
||||
|
||||
private List<String> getFontNames() {
|
||||
try {
|
||||
Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
|
||||
.getResources("classpath:static/fonts/*.woff2");
|
||||
|
||||
return Arrays.stream(resources)
|
||||
.map(resource -> {
|
||||
try {
|
||||
String filename = resource.getFilename();
|
||||
return filename.substring(0, filename.length() - 6); // Remove .woff2 extension
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error processing filename", e);
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to read font directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@GetMapping("/crop")
|
||||
@Hidden
|
||||
public String cropForm(Model model) {
|
||||
model.addAttribute("currentPage", "crop");
|
||||
return "crop";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/auto-split-pdf")
|
||||
@Hidden
|
||||
public String autoSPlitPDFForm(Model model) {
|
||||
model.addAttribute("currentPage", "auto-split-pdf");
|
||||
return "auto-split-pdf";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
|
||||
@Controller
|
||||
public class HomeWebController {
|
||||
|
||||
@GetMapping("/about")
|
||||
@Hidden
|
||||
public String gameForm(Model model) {
|
||||
model.addAttribute("currentPage", "about");
|
||||
return "about";
|
||||
}
|
||||
|
||||
|
||||
|
||||
@GetMapping("/")
|
||||
public String home(Model model) {
|
||||
model.addAttribute("currentPage", "home");
|
||||
return "home";
|
||||
}
|
||||
|
||||
@GetMapping("/home")
|
||||
public String root(Model model) {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
@Autowired
|
||||
ApplicationProperties applicationProperties;
|
||||
|
||||
|
||||
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@ResponseBody
|
||||
@Hidden
|
||||
public String getRobotsTxt() {
|
||||
Boolean allowGoogle = applicationProperties.getSystem().getGooglevisibility();
|
||||
if(Boolean.TRUE.equals(allowGoogle)) {
|
||||
return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /";
|
||||
} else {
|
||||
return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,16 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
@@ -12,13 +20,34 @@ import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.Meter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import stirling.software.SPDF.config.StartupApplicationListener;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1")
|
||||
@Tag(name = "API", description = "Info APIs")
|
||||
public class MetricsController {
|
||||
|
||||
|
||||
@Autowired
|
||||
ApplicationProperties applicationProperties;
|
||||
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
private boolean metricsEnabled;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
Boolean metricsEnabled = applicationProperties.getMetrics().getEnabled();
|
||||
if(metricsEnabled == null)
|
||||
metricsEnabled = true;
|
||||
this.metricsEnabled = metricsEnabled;
|
||||
}
|
||||
|
||||
public MetricsController(MeterRegistry meterRegistry) {
|
||||
this.meterRegistry = meterRegistry;
|
||||
}
|
||||
@@ -26,55 +55,211 @@ public class MetricsController {
|
||||
@GetMapping("/status")
|
||||
@Operation(summary = "Application status and version",
|
||||
description = "This endpoint returns the status of the application and its version number.")
|
||||
public Map<String, String> getStatus() {
|
||||
public ResponseEntity<?> getStatus() {
|
||||
if (!metricsEnabled) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
|
||||
}
|
||||
|
||||
Map<String, String> status = new HashMap<>();
|
||||
status.put("status", "UP");
|
||||
status.put("version", getClass().getPackage().getImplementationVersion());
|
||||
return status;
|
||||
return ResponseEntity.ok(status);
|
||||
}
|
||||
|
||||
@GetMapping("/loads")
|
||||
@Operation(summary = "GET request count",
|
||||
description = "This endpoint returns the total count of GET requests or the count of GET requests for a specific endpoint.")
|
||||
public Double getPageLoads(@RequestParam Optional<String> endpoint) {
|
||||
try {
|
||||
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.");
|
||||
}
|
||||
try {
|
||||
|
||||
double count = 0.0;
|
||||
|
||||
for (Meter meter : meterRegistry.getMeters()) {
|
||||
if (meter.getId().getName().equals("http.requests")) {
|
||||
String method = meter.getId().getTag("method");
|
||||
if (method != null && method.equals("GET")) {
|
||||
|
||||
if (endpoint.isPresent() && !endpoint.get().isBlank()) {
|
||||
if(!endpoint.get().startsWith("/")) {
|
||||
endpoint = Optional.of("/" + endpoint.get());
|
||||
}
|
||||
System.out.println("loads " + endpoint.get() + " vs " + meter.getId().getTag("uri"));
|
||||
if(endpoint.get().equals(meter.getId().getTag("uri"))){
|
||||
if (meter instanceof Counter) {
|
||||
count += ((Counter) meter).count();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (meter instanceof Counter) {
|
||||
count += ((Counter) meter).count();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(count);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/loads/all")
|
||||
@Operation(summary = "GET requests count for all endpoints",
|
||||
description = "This endpoint returns the count of GET requests for each endpoint.")
|
||||
public ResponseEntity<?> getAllEndpointLoads() {
|
||||
if (!metricsEnabled) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
|
||||
}
|
||||
try {
|
||||
Map<String, Double> counts = new HashMap<>();
|
||||
|
||||
for (Meter meter : meterRegistry.getMeters()) {
|
||||
if (meter.getId().getName().equals("http.requests")) {
|
||||
String method = meter.getId().getTag("method");
|
||||
if (method != null && method.equals("GET")) {
|
||||
if (meter instanceof Counter) {
|
||||
count += ((Counter) meter).count();
|
||||
String uri = meter.getId().getTag("uri");
|
||||
if (uri != null) {
|
||||
double currentCount = counts.getOrDefault(uri, 0.0);
|
||||
if (meter instanceof Counter) {
|
||||
currentCount += ((Counter) meter).count();
|
||||
}
|
||||
counts.put(uri, currentCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
List<EndpointCount> results = counts.entrySet().stream()
|
||||
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
|
||||
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(results);
|
||||
} catch (Exception e) {
|
||||
return -1.0;
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
public class EndpointCount {
|
||||
private String endpoint;
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/requests")
|
||||
@Operation(summary = "POST request count",
|
||||
description = "This endpoint returns the total count of POST requests or the count of POST requests for a specific endpoint.")
|
||||
public Double getTotalRequests(@RequestParam Optional<String> endpoint) {
|
||||
try {
|
||||
Counter counter;
|
||||
if (endpoint.isPresent()) {
|
||||
counter = meterRegistry.get("http.requests")
|
||||
.tags("method", "POST", "uri", endpoint.get()).counter();
|
||||
} else {
|
||||
counter = meterRegistry.get("http.requests")
|
||||
.tags("method", "POST").counter();
|
||||
}
|
||||
return counter.count();
|
||||
} catch (Exception e) {
|
||||
return -1.0;
|
||||
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.");
|
||||
}
|
||||
try {
|
||||
double count = 0.0;
|
||||
|
||||
for (Meter meter : meterRegistry.getMeters()) {
|
||||
if (meter.getId().getName().equals("http.requests")) {
|
||||
String method = meter.getId().getTag("method");
|
||||
if (method != null && method.equals("POST")) {
|
||||
if (endpoint.isPresent() && !endpoint.get().isBlank()) {
|
||||
if (!endpoint.get().startsWith("/")) {
|
||||
endpoint = Optional.of("/" + endpoint.get());
|
||||
}
|
||||
if (endpoint.get().equals(meter.getId().getTag("uri"))) {
|
||||
if (meter instanceof Counter) {
|
||||
count += ((Counter) meter).count();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (meter instanceof Counter) {
|
||||
count += ((Counter) meter).count();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ResponseEntity.ok(count);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(-1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/requests/all")
|
||||
@Operation(summary = "POST requests count for all endpoints",
|
||||
description = "This endpoint returns the count of POST requests for each endpoint.")
|
||||
public ResponseEntity<?> getAllPostRequests() {
|
||||
if (!metricsEnabled) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
|
||||
}
|
||||
try {
|
||||
Map<String, Double> counts = new HashMap<>();
|
||||
|
||||
for (Meter meter : meterRegistry.getMeters()) {
|
||||
if (meter.getId().getName().equals("http.requests")) {
|
||||
String method = meter.getId().getTag("method");
|
||||
if (method != null && method.equals("POST")) {
|
||||
String uri = meter.getId().getTag("uri");
|
||||
if (uri != null) {
|
||||
double currentCount = counts.getOrDefault(uri, 0.0);
|
||||
if (meter instanceof Counter) {
|
||||
currentCount += ((Counter) meter).count();
|
||||
}
|
||||
counts.put(uri, currentCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<EndpointCount> results = counts.entrySet().stream()
|
||||
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
|
||||
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(results);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/uptime")
|
||||
public ResponseEntity<?> getUptime() {
|
||||
if (!metricsEnabled) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
Duration uptime = Duration.between(StartupApplicationListener.startTime, now);
|
||||
return ResponseEntity.ok(formatDuration(uptime));
|
||||
}
|
||||
|
||||
private String formatDuration(Duration duration) {
|
||||
long days = duration.toDays();
|
||||
long hours = duration.toHoursPart();
|
||||
long minutes = duration.toMinutesPart();
|
||||
long seconds = duration.toSecondsPart();
|
||||
return String.format("%dd %dh %dm %ds", days, hours, minutes, seconds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@Controller
|
||||
@Tag(name = "Other", description = "Other APIs")
|
||||
public class OtherWebController {
|
||||
@GetMapping("/compress-pdf")
|
||||
@Hidden
|
||||
@@ -29,7 +31,22 @@ public class OtherWebController {
|
||||
modelAndView.addObject("currentPage", "extract-image-scans");
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/show-javascript")
|
||||
@Hidden
|
||||
public String extractJavascriptForm(Model model) {
|
||||
model.addAttribute("currentPage", "show-javascript");
|
||||
return "other/show-javascript";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/add-page-numbers")
|
||||
@Hidden
|
||||
public String addPageNumbersForm(Model model) {
|
||||
model.addAttribute("currentPage", "add-page-numbers");
|
||||
return "other/add-page-numbers";
|
||||
}
|
||||
|
||||
@GetMapping("/extract-images")
|
||||
@Hidden
|
||||
public String extractImagesForm(Model model) {
|
||||
@@ -131,4 +148,13 @@ public class OtherWebController {
|
||||
return "other/auto-crop";
|
||||
}
|
||||
|
||||
@GetMapping("/auto-rename")
|
||||
@Hidden
|
||||
public String autoRenameForm(Model model) {
|
||||
model.addAttribute("currentPage", "auto-rename");
|
||||
return "other/auto-rename";
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,44 +1,68 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
@Controller
|
||||
public class SecurityWebController {
|
||||
@GetMapping("/add-password")
|
||||
@Hidden
|
||||
public String addPasswordForm(Model model) {
|
||||
model.addAttribute("currentPage", "add-password");
|
||||
return "security/add-password";
|
||||
}
|
||||
@GetMapping("/change-permissions")
|
||||
@Hidden
|
||||
public String permissionsForm(Model model) {
|
||||
model.addAttribute("currentPage", "change-permissions");
|
||||
return "security/change-permissions";
|
||||
}
|
||||
|
||||
@GetMapping("/remove-password")
|
||||
@Hidden
|
||||
public String removePasswordForm(Model model) {
|
||||
model.addAttribute("currentPage", "remove-password");
|
||||
return "security/remove-password";
|
||||
}
|
||||
|
||||
@GetMapping("/add-watermark")
|
||||
@Hidden
|
||||
public String addWatermarkForm(Model model) {
|
||||
model.addAttribute("currentPage", "add-watermark");
|
||||
return "security/add-watermark";
|
||||
}
|
||||
|
||||
@GetMapping("/cert-sign")
|
||||
@Hidden
|
||||
public String certSignForm(Model model) {
|
||||
model.addAttribute("currentPage", "cert-sign");
|
||||
return "security/cert-sign";
|
||||
}
|
||||
}
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@Controller
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
public class SecurityWebController {
|
||||
|
||||
@GetMapping("/auto-redact")
|
||||
@Hidden
|
||||
public String autoRedactForm(Model model) {
|
||||
model.addAttribute("currentPage", "auto-redact");
|
||||
return "security/auto-redact";
|
||||
}
|
||||
|
||||
@GetMapping("/add-password")
|
||||
@Hidden
|
||||
public String addPasswordForm(Model model) {
|
||||
model.addAttribute("currentPage", "add-password");
|
||||
return "security/add-password";
|
||||
}
|
||||
@GetMapping("/change-permissions")
|
||||
@Hidden
|
||||
public String permissionsForm(Model model) {
|
||||
model.addAttribute("currentPage", "change-permissions");
|
||||
return "security/change-permissions";
|
||||
}
|
||||
|
||||
@GetMapping("/remove-password")
|
||||
@Hidden
|
||||
public String removePasswordForm(Model model) {
|
||||
model.addAttribute("currentPage", "remove-password");
|
||||
return "security/remove-password";
|
||||
}
|
||||
|
||||
@GetMapping("/add-watermark")
|
||||
@Hidden
|
||||
public String addWatermarkForm(Model model) {
|
||||
model.addAttribute("currentPage", "add-watermark");
|
||||
return "security/add-watermark";
|
||||
}
|
||||
|
||||
@GetMapping("/cert-sign")
|
||||
@Hidden
|
||||
public String certSignForm(Model model) {
|
||||
model.addAttribute("currentPage", "cert-sign");
|
||||
return "security/cert-sign";
|
||||
}
|
||||
|
||||
@GetMapping("/sanitize-pdf")
|
||||
@Hidden
|
||||
public String sanitizeForm(Model model) {
|
||||
model.addAttribute("currentPage", "sanitize-pdf");
|
||||
return "security/sanitize-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/get-info-on-pdf")
|
||||
@Hidden
|
||||
public String getInfo(Model model) {
|
||||
model.addAttribute("currentPage", "get-info-on-pdf");
|
||||
return "security/get-info-on-pdf";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package stirling.software.SPDF.model;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
private final Object principal;
|
||||
private Object credentials;
|
||||
|
||||
public ApiKeyAuthenticationToken(String apiKey) {
|
||||
super(null);
|
||||
this.principal = null;
|
||||
this.credentials = apiKey;
|
||||
setAuthenticated(false);
|
||||
}
|
||||
|
||||
public ApiKeyAuthenticationToken(Object principal, String apiKey, Collection<? extends GrantedAuthority> authorities) {
|
||||
super(authorities);
|
||||
this.principal = principal; // principal can be a UserDetails object
|
||||
this.credentials = apiKey;
|
||||
super.setAuthenticated(true); // this authentication is trusted
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return credentials;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return principal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
|
||||
if (isAuthenticated) {
|
||||
throw new IllegalArgumentException("Cannot set this token to trusted. Use constructor which takes a GrantedAuthority list instead.");
|
||||
}
|
||||
super.setAuthenticated(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eraseCredentials() {
|
||||
super.eraseCredentials();
|
||||
credentials = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
package stirling.software.SPDF.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
|
||||
import stirling.software.SPDF.config.YamlPropertySourceFactory;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "")
|
||||
@PropertySource(value = "file:./configs/settings.yml", factory = YamlPropertySourceFactory.class)
|
||||
public class ApplicationProperties {
|
||||
private Security security;
|
||||
private System system;
|
||||
private Ui ui;
|
||||
private Endpoints endpoints;
|
||||
private Metrics metrics;
|
||||
private AutomaticallyGenerated automaticallyGenerated;
|
||||
private AutoPipeline autoPipeline;
|
||||
|
||||
public AutoPipeline getAutoPipeline() {
|
||||
return autoPipeline != null ? autoPipeline : new AutoPipeline();
|
||||
}
|
||||
|
||||
public void setAutoPipeline(AutoPipeline autoPipeline) {
|
||||
this.autoPipeline = autoPipeline;
|
||||
}
|
||||
|
||||
public Security getSecurity() {
|
||||
return security != null ? security : new Security();
|
||||
}
|
||||
|
||||
public void setSecurity(Security security) {
|
||||
this.security = security;
|
||||
}
|
||||
|
||||
public System getSystem() {
|
||||
return system != null ? system : new System();
|
||||
}
|
||||
|
||||
public void setSystem(System system) {
|
||||
this.system = system;
|
||||
}
|
||||
|
||||
public Ui getUi() {
|
||||
return ui != null ? ui : new Ui();
|
||||
}
|
||||
|
||||
public void setUi(Ui ui) {
|
||||
this.ui = ui;
|
||||
}
|
||||
|
||||
public Endpoints getEndpoints() {
|
||||
return endpoints != null ? endpoints : new Endpoints();
|
||||
}
|
||||
|
||||
public void setEndpoints(Endpoints endpoints) {
|
||||
this.endpoints = endpoints;
|
||||
}
|
||||
|
||||
public Metrics getMetrics() {
|
||||
return metrics != null ? metrics : new Metrics();
|
||||
}
|
||||
|
||||
public void setMetrics(Metrics metrics) {
|
||||
this.metrics = metrics;
|
||||
}
|
||||
|
||||
public AutomaticallyGenerated getAutomaticallyGenerated() {
|
||||
return automaticallyGenerated != null ? automaticallyGenerated : new AutomaticallyGenerated();
|
||||
}
|
||||
|
||||
public void setAutomaticallyGenerated(AutomaticallyGenerated automaticallyGenerated) {
|
||||
this.automaticallyGenerated = automaticallyGenerated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ApplicationProperties [security=" + security + ", system=" + system + ", ui=" + ui + ", endpoints="
|
||||
+ endpoints + ", metrics=" + metrics + ", automaticallyGenerated=" + automaticallyGenerated
|
||||
+ ", autoPipeline=" + autoPipeline + "]";
|
||||
}
|
||||
|
||||
public static class AutoPipeline {
|
||||
private String outputFolder;
|
||||
|
||||
public String getOutputFolder() {
|
||||
return outputFolder;
|
||||
}
|
||||
|
||||
public void setOutputFolder(String outputFolder) {
|
||||
this.outputFolder = outputFolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AutoPipeline [outputFolder=" + outputFolder + "]";
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
public static class Security {
|
||||
private Boolean enableLogin;
|
||||
private InitialLogin initialLogin;
|
||||
private Boolean csrfDisabled;
|
||||
|
||||
public Boolean getEnableLogin() {
|
||||
return enableLogin;
|
||||
}
|
||||
|
||||
public void setEnableLogin(Boolean enableLogin) {
|
||||
this.enableLogin = enableLogin;
|
||||
}
|
||||
|
||||
public InitialLogin getInitialLogin() {
|
||||
return initialLogin != null ? initialLogin : new InitialLogin();
|
||||
}
|
||||
|
||||
public void setInitialLogin(InitialLogin initialLogin) {
|
||||
this.initialLogin = initialLogin;
|
||||
}
|
||||
|
||||
public Boolean getCsrfDisabled() {
|
||||
return csrfDisabled;
|
||||
}
|
||||
|
||||
public void setCsrfDisabled(Boolean csrfDisabled) {
|
||||
this.csrfDisabled = csrfDisabled;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Security [enableLogin=" + enableLogin + ", initialLogin=" + initialLogin + ", csrfDisabled="
|
||||
+ csrfDisabled + "]";
|
||||
}
|
||||
|
||||
|
||||
public static class InitialLogin {
|
||||
|
||||
private String username;
|
||||
private String password;
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "InitialLogin [username=" + username + ", password=" + (password != null && !password.isEmpty() ? "MASKED" : "NULL") + "]";
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static class System {
|
||||
private String defaultLocale;
|
||||
private Boolean googlevisibility;
|
||||
private String rootURIPath;
|
||||
private String customStaticFilePath;
|
||||
private Integer maxFileSize;
|
||||
|
||||
public String getDefaultLocale() {
|
||||
return defaultLocale;
|
||||
}
|
||||
|
||||
public void setDefaultLocale(String defaultLocale) {
|
||||
this.defaultLocale = defaultLocale;
|
||||
}
|
||||
|
||||
public Boolean getGooglevisibility() {
|
||||
return googlevisibility;
|
||||
}
|
||||
|
||||
public void setGooglevisibility(Boolean googlevisibility) {
|
||||
this.googlevisibility = googlevisibility;
|
||||
}
|
||||
|
||||
public String getRootURIPath() {
|
||||
return rootURIPath;
|
||||
}
|
||||
|
||||
public void setRootURIPath(String rootURIPath) {
|
||||
this.rootURIPath = rootURIPath;
|
||||
}
|
||||
|
||||
public String getCustomStaticFilePath() {
|
||||
return customStaticFilePath;
|
||||
}
|
||||
|
||||
public void setCustomStaticFilePath(String customStaticFilePath) {
|
||||
this.customStaticFilePath = customStaticFilePath;
|
||||
}
|
||||
|
||||
public Integer getMaxFileSize() {
|
||||
return maxFileSize;
|
||||
}
|
||||
|
||||
public void setMaxFileSize(Integer maxFileSize) {
|
||||
this.maxFileSize = maxFileSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "System [defaultLocale=" + defaultLocale + ", googlevisibility=" + googlevisibility + ", rootURIPath="
|
||||
+ rootURIPath + ", customStaticFilePath=" + customStaticFilePath + ", maxFileSize=" + maxFileSize
|
||||
+ "]";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static class Ui {
|
||||
private String appName;
|
||||
private String homeDescription;
|
||||
private String appNameNavbar;
|
||||
|
||||
public String getAppName() {
|
||||
return appName;
|
||||
}
|
||||
|
||||
public void setAppName(String appName) {
|
||||
this.appName = appName;
|
||||
}
|
||||
|
||||
public String getHomeDescription() {
|
||||
return homeDescription;
|
||||
}
|
||||
|
||||
public void setHomeDescription(String homeDescription) {
|
||||
this.homeDescription = homeDescription;
|
||||
}
|
||||
|
||||
public String getAppNameNavbar() {
|
||||
return appNameNavbar;
|
||||
}
|
||||
|
||||
public void setAppNameNavbar(String appNameNavbar) {
|
||||
this.appNameNavbar = appNameNavbar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UserInterface [appName=" + appName + ", homeDescription=" + homeDescription + ", appNameNavbar=" + appNameNavbar + "]";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class Endpoints {
|
||||
private List<String> toRemove;
|
||||
private List<String> groupsToRemove;
|
||||
|
||||
public List<String> getToRemove() {
|
||||
return toRemove;
|
||||
}
|
||||
|
||||
public void setToRemove(List<String> toRemove) {
|
||||
this.toRemove = toRemove;
|
||||
}
|
||||
|
||||
public List<String> getGroupsToRemove() {
|
||||
return groupsToRemove;
|
||||
}
|
||||
|
||||
public void setGroupsToRemove(List<String> groupsToRemove) {
|
||||
this.groupsToRemove = groupsToRemove;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Endpoints [toRemove=" + toRemove + ", groupsToRemove=" + groupsToRemove + "]";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static class Metrics {
|
||||
private Boolean enabled;
|
||||
|
||||
public Boolean getEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Metrics [enabled=" + enabled + "]";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static class AutomaticallyGenerated {
|
||||
private String key;
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AutomaticallyGenerated [key=" + (key != null && !key.isEmpty() ? "MASKED" : "NULL") + "]";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
64
src/main/java/stirling/software/SPDF/model/Authority.java
Normal file
64
src/main/java/stirling/software/SPDF/model/Authority.java
Normal file
@@ -0,0 +1,64 @@
|
||||
package stirling.software.SPDF.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "authorities")
|
||||
public class Authority {
|
||||
|
||||
public Authority() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
public Authority(String authority, User user) {
|
||||
this.authority = authority;
|
||||
this.user = user;
|
||||
user.getAuthorities().add(this);
|
||||
}
|
||||
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "authority")
|
||||
private String authority;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "user_id")
|
||||
private User user;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getAuthority() {
|
||||
return authority;
|
||||
}
|
||||
|
||||
public void setAuthority(String authority) {
|
||||
this.authority = authority;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
42
src/main/java/stirling/software/SPDF/model/PDFText.java
Normal file
42
src/main/java/stirling/software/SPDF/model/PDFText.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package stirling.software.SPDF.model;
|
||||
public class PDFText {
|
||||
private final int pageIndex;
|
||||
private final float x1;
|
||||
private final float y1;
|
||||
private final float x2;
|
||||
private final float y2;
|
||||
private final String text;
|
||||
|
||||
public PDFText(int pageIndex, float x1, float y1, float x2, float y2, String text) {
|
||||
this.pageIndex = pageIndex;
|
||||
this.x1 = x1;
|
||||
this.y1 = y1;
|
||||
this.x2 = x2;
|
||||
this.y2 = y2;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public int getPageIndex() {
|
||||
return pageIndex;
|
||||
}
|
||||
|
||||
public float getX1() {
|
||||
return x1;
|
||||
}
|
||||
|
||||
public float getY1() {
|
||||
return y1;
|
||||
}
|
||||
|
||||
public float getX2() {
|
||||
return x2;
|
||||
}
|
||||
|
||||
public float getY2() {
|
||||
return y2;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package stirling.software.SPDF.model;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "persistent_logins")
|
||||
public class PersistentLogin {
|
||||
|
||||
@Id
|
||||
@Column(name = "series")
|
||||
private String series;
|
||||
|
||||
@Column(name = "username", length = 64, nullable = false)
|
||||
private String username;
|
||||
|
||||
@Column(name = "token", length = 64, nullable = false)
|
||||
private String token;
|
||||
|
||||
@Column(name = "last_used", nullable = false)
|
||||
private Date lastUsed;
|
||||
|
||||
public String getSeries() {
|
||||
return series;
|
||||
}
|
||||
|
||||
public void setSeries(String series) {
|
||||
this.series = series;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public Date getLastUsed() {
|
||||
return lastUsed;
|
||||
}
|
||||
|
||||
public void setLastUsed(Date lastUsed) {
|
||||
this.lastUsed = lastUsed;
|
||||
}
|
||||
|
||||
|
||||
// Getters, setters, etc.
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package stirling.software.SPDF.model;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class PipelineConfig {
|
||||
private String name;
|
||||
|
||||
@JsonProperty("pipeline")
|
||||
private List<PipelineOperation> operations;
|
||||
|
||||
private String outputDir;
|
||||
|
||||
@JsonProperty("outputFileName")
|
||||
private String outputPattern;
|
||||
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public List<PipelineOperation> getOperations() {
|
||||
return operations;
|
||||
}
|
||||
|
||||
public void setOperations(List<PipelineOperation> operations) {
|
||||
this.operations = operations;
|
||||
}
|
||||
|
||||
public String getOutputDir() {
|
||||
return outputDir;
|
||||
}
|
||||
|
||||
public void setOutputDir(String outputDir) {
|
||||
this.outputDir = outputDir;
|
||||
}
|
||||
|
||||
public String getOutputPattern() {
|
||||
return outputPattern;
|
||||
}
|
||||
|
||||
public void setOutputPattern(String outputPattern) {
|
||||
this.outputPattern = outputPattern;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package stirling.software.SPDF.model;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class PipelineOperation {
|
||||
private String operation;
|
||||
private Map<String, Object> parameters;
|
||||
|
||||
|
||||
public String getOperation() {
|
||||
return operation;
|
||||
}
|
||||
|
||||
public void setOperation(String operation) {
|
||||
this.operation = operation;
|
||||
}
|
||||
|
||||
public Map<String, Object> getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public void setParameters(Map<String, Object> parameters) {
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PipelineOperation [operation=" + operation + ", parameters=" + parameters + "]";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
50
src/main/java/stirling/software/SPDF/model/Role.java
Normal file
50
src/main/java/stirling/software/SPDF/model/Role.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package stirling.software.SPDF.model;
|
||||
public enum Role {
|
||||
|
||||
// Unlimited access
|
||||
ADMIN("ROLE_ADMIN", Integer.MAX_VALUE, Integer.MAX_VALUE),
|
||||
|
||||
// Unlimited access
|
||||
USER("ROLE_USER", Integer.MAX_VALUE, Integer.MAX_VALUE),
|
||||
|
||||
// 40 API calls Per Day, 40 web calls
|
||||
LIMITED_API_USER("ROLE_LIMITED_API_USER", 40, 40),
|
||||
|
||||
// 20 API calls Per Day, 20 web calls
|
||||
EXTRA_LIMITED_API_USER("ROLE_EXTRA_LIMITED_API_USER", 20, 20),
|
||||
|
||||
// 0 API calls per day and 20 web calls
|
||||
WEB_ONLY_USER("ROLE_WEB_ONLY_USER", 0, 20);
|
||||
|
||||
private final String roleId;
|
||||
private final int apiCallsPerDay;
|
||||
private final int webCallsPerDay;
|
||||
|
||||
Role(String roleId, int apiCallsPerDay, int webCallsPerDay) {
|
||||
this.roleId = roleId;
|
||||
this.apiCallsPerDay = apiCallsPerDay;
|
||||
this.webCallsPerDay = webCallsPerDay;
|
||||
}
|
||||
|
||||
public String getRoleId() {
|
||||
return roleId;
|
||||
}
|
||||
|
||||
public int getApiCallsPerDay() {
|
||||
return apiCallsPerDay;
|
||||
}
|
||||
|
||||
public int getWebCallsPerDay() {
|
||||
return webCallsPerDay;
|
||||
}
|
||||
|
||||
public static Role fromString(String roleId) {
|
||||
for (Role role : Role.values()) {
|
||||
if (role.getRoleId().equalsIgnoreCase(roleId)) {
|
||||
return role;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("No Role defined for id: " + roleId);
|
||||
}
|
||||
|
||||
}
|
||||
124
src/main/java/stirling/software/SPDF/model/User.java
Normal file
124
src/main/java/stirling/software/SPDF/model/User.java
Normal file
@@ -0,0 +1,124 @@
|
||||
package stirling.software.SPDF.model;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.CollectionTable;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.ElementCollection;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.MapKeyColumn;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.Table;
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "user_id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "username", unique = true)
|
||||
private String username;
|
||||
|
||||
@Column(name = "password")
|
||||
private String password;
|
||||
|
||||
@Column(name = "apiKey")
|
||||
private String apiKey;
|
||||
|
||||
@Column(name = "enabled")
|
||||
private boolean enabled;
|
||||
|
||||
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user")
|
||||
private Set<Authority> authorities = new HashSet<>();
|
||||
|
||||
@ElementCollection
|
||||
@MapKeyColumn(name = "setting_key")
|
||||
@Column(name = "setting_value")
|
||||
@CollectionTable(name = "user_settings", joinColumns = @JoinColumn(name = "user_id"))
|
||||
private Map<String, String> settings = new HashMap<>(); // Key-value pairs of settings.
|
||||
|
||||
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getApiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
public void setApiKey(String apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
public Map<String, String> getSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
public void setSettings(Map<String, String> settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public Set<Authority> getAuthorities() {
|
||||
return authorities;
|
||||
}
|
||||
|
||||
public void setAuthorities(Set<Authority> authorities) {
|
||||
this.authorities = authorities;
|
||||
}
|
||||
|
||||
public void addAuthorities(Set<Authority> authorities) {
|
||||
this.authorities.addAll(authorities);
|
||||
}
|
||||
public void addAuthority(Authority authorities) {
|
||||
this.authorities.add(authorities);
|
||||
}
|
||||
|
||||
public String getRolesAsString() {
|
||||
return this.authorities.stream()
|
||||
.map(Authority::getAuthority)
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
90
src/main/java/stirling/software/SPDF/pdf/TextFinder.java
Normal file
90
src/main/java/stirling/software/SPDF/pdf/TextFinder.java
Normal file
@@ -0,0 +1,90 @@
|
||||
package stirling.software.SPDF.pdf;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.apache.pdfbox.text.TextPosition;
|
||||
|
||||
import stirling.software.SPDF.model.PDFText;
|
||||
|
||||
public class TextFinder extends PDFTextStripper {
|
||||
|
||||
private final String searchText;
|
||||
private final boolean useRegex;
|
||||
private final boolean wholeWordSearch;
|
||||
private final List<PDFText> textOccurrences = new ArrayList<>();
|
||||
|
||||
public TextFinder(String searchText, boolean useRegex, boolean wholeWordSearch) throws IOException {
|
||||
this.searchText = searchText.toLowerCase();
|
||||
this.useRegex = useRegex;
|
||||
this.wholeWordSearch = wholeWordSearch;
|
||||
setSortByPosition(true);
|
||||
}
|
||||
|
||||
private List<Integer> findOccurrencesInText(String searchText, String content) {
|
||||
List<Integer> indexes = new ArrayList<>();
|
||||
Pattern pattern;
|
||||
|
||||
if (useRegex) {
|
||||
// Use regex-based search
|
||||
pattern = wholeWordSearch
|
||||
? Pattern.compile("(\\b|_|\\.)" + searchText + "(\\b|_|\\.)")
|
||||
: Pattern.compile(searchText);
|
||||
} else {
|
||||
// Use normal text search
|
||||
pattern = wholeWordSearch
|
||||
? Pattern.compile("(\\b|_|\\.)" + Pattern.quote(searchText) + "(\\b|_|\\.)")
|
||||
: Pattern.compile(Pattern.quote(searchText));
|
||||
}
|
||||
|
||||
Matcher matcher = pattern.matcher(content);
|
||||
while (matcher.find()) {
|
||||
indexes.add(matcher.start());
|
||||
}
|
||||
return indexes;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeString(String text, List<TextPosition> textPositions) {
|
||||
for (Integer index : findOccurrencesInText(searchText, text.toLowerCase())) {
|
||||
if (index + searchText.length() <= textPositions.size()) {
|
||||
// Initial values based on the first character
|
||||
TextPosition first = textPositions.get(index);
|
||||
float minX = first.getX();
|
||||
float minY = first.getY();
|
||||
float maxX = first.getX() + first.getWidth();
|
||||
float maxY = first.getY() + first.getHeight();
|
||||
|
||||
// Loop over the rest of the characters and adjust bounding box values
|
||||
for (int i = index; i < index + searchText.length(); i++) {
|
||||
TextPosition position = textPositions.get(i);
|
||||
minX = Math.min(minX, position.getX());
|
||||
minY = Math.min(minY, position.getY());
|
||||
maxX = Math.max(maxX, position.getX() + position.getWidth());
|
||||
maxY = Math.max(maxY, position.getY() + position.getHeight());
|
||||
}
|
||||
|
||||
textOccurrences.add(new PDFText(
|
||||
getCurrentPageNo() - 1,
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
text
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<PDFText> getTextLocations(PDDocument document) throws Exception {
|
||||
this.getText(document);
|
||||
System.out.println("Found " + textOccurrences.size() + " occurrences of '" + searchText + "' in the document.");
|
||||
|
||||
return textOccurrences;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package stirling.software.SPDF.repository;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import stirling.software.SPDF.model.Authority;
|
||||
|
||||
public interface AuthorityRepository extends JpaRepository<Authority, Long> {
|
||||
//Set<Authority> findByUsername(String username);
|
||||
Set<Authority> findByUser_Username(String username);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package stirling.software.SPDF.repository;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken;
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||
|
||||
import stirling.software.SPDF.model.PersistentLogin;
|
||||
|
||||
public class JPATokenRepositoryImpl implements PersistentTokenRepository {
|
||||
|
||||
@Autowired
|
||||
private PersistentLoginRepository persistentLoginRepository;
|
||||
|
||||
@Override
|
||||
public void createNewToken(PersistentRememberMeToken token) {
|
||||
PersistentLogin newToken = new PersistentLogin();
|
||||
newToken.setSeries(token.getSeries());
|
||||
newToken.setUsername(token.getUsername());
|
||||
newToken.setToken(token.getTokenValue());
|
||||
newToken.setLastUsed(token.getDate());
|
||||
persistentLoginRepository.save(newToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateToken(String series, String tokenValue, Date lastUsed) {
|
||||
PersistentLogin existingToken = persistentLoginRepository.findById(series).orElse(null);
|
||||
if (existingToken != null) {
|
||||
existingToken.setToken(tokenValue);
|
||||
existingToken.setLastUsed(lastUsed);
|
||||
persistentLoginRepository.save(existingToken);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PersistentRememberMeToken getTokenForSeries(String seriesId) {
|
||||
PersistentLogin token = persistentLoginRepository.findById(seriesId).orElse(null);
|
||||
if (token != null) {
|
||||
return new PersistentRememberMeToken(token.getUsername(), token.getSeries(), token.getToken(), token.getLastUsed());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUserTokens(String username) {
|
||||
for (PersistentLogin token : persistentLoginRepository.findAll()) {
|
||||
if (token.getUsername().equals(username)) {
|
||||
persistentLoginRepository.delete(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package stirling.software.SPDF.repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import stirling.software.SPDF.model.PersistentLogin;
|
||||
|
||||
public interface PersistentLoginRepository extends JpaRepository<PersistentLogin, String> {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package stirling.software.SPDF.repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import stirling.software.SPDF.model.User;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, String> {
|
||||
Optional<User> findByUsername(String username);
|
||||
User findByApiKey(String apiKey);
|
||||
}
|
||||
|
||||
95
src/main/java/stirling/software/SPDF/utils/FileToPdf.java
Normal file
95
src/main/java/stirling/software/SPDF/utils/FileToPdf.java
Normal file
@@ -0,0 +1,95 @@
|
||||
package stirling.software.SPDF.utils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
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.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||
|
||||
public class FileToPdf {
|
||||
public static byte[] convertHtmlToPdf(byte[] fileBytes, String fileName) throws IOException, InterruptedException {
|
||||
|
||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||
Path tempInputFile = null;
|
||||
byte[] pdfBytes;
|
||||
try {
|
||||
if (fileName.endsWith(".html")) {
|
||||
tempInputFile = Files.createTempFile("input_", ".html");
|
||||
Files.write(tempInputFile, fileBytes);
|
||||
} else {
|
||||
tempInputFile = unzipAndGetMainHtml(fileBytes);
|
||||
}
|
||||
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("weasyprint");
|
||||
command.add(tempInputFile.toString());
|
||||
command.add(tempOutputFile.toString());
|
||||
ProcessExecutorResult returnCode;
|
||||
if (fileName.endsWith(".zip")) {
|
||||
returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
|
||||
.runCommandWithOutputHandling(command, tempInputFile.getParent().toFile());
|
||||
} else {
|
||||
|
||||
returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
|
||||
.runCommandWithOutputHandling(command);
|
||||
}
|
||||
|
||||
pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||
} finally {
|
||||
// Clean up temporary files
|
||||
Files.delete(tempOutputFile);
|
||||
Files.delete(tempInputFile);
|
||||
|
||||
if (fileName.endsWith(".zip")) {
|
||||
GeneralUtils.deleteDirectory(tempInputFile.getParent());
|
||||
}
|
||||
}
|
||||
|
||||
return pdfBytes;
|
||||
}
|
||||
|
||||
|
||||
private static Path unzipAndGetMainHtml(byte[] fileBytes) throws IOException {
|
||||
Path tempDirectory = Files.createTempDirectory("unzipped_");
|
||||
try (ZipInputStream zipIn = new ZipInputStream(new ByteArrayInputStream(fileBytes))) {
|
||||
ZipEntry entry = zipIn.getNextEntry();
|
||||
while (entry != null) {
|
||||
Path filePath = tempDirectory.resolve(entry.getName());
|
||||
if (entry.isDirectory()) {
|
||||
Files.createDirectories(filePath); // Explicitly create the directory structure
|
||||
} else {
|
||||
Files.createDirectories(filePath.getParent()); // Create parent directories if they don't exist
|
||||
Files.copy(zipIn, filePath);
|
||||
}
|
||||
zipIn.closeEntry();
|
||||
entry = zipIn.getNextEntry();
|
||||
}
|
||||
}
|
||||
|
||||
//search for the main HTML file.
|
||||
try (Stream<Path> walk = Files.walk(tempDirectory)) {
|
||||
List<Path> htmlFiles = walk.filter(file -> file.toString().endsWith(".html"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (htmlFiles.isEmpty()) {
|
||||
throw new IOException("No HTML files found in the unzipped directory.");
|
||||
}
|
||||
|
||||
// Prioritize 'index.html' if it exists, otherwise use the first .html file
|
||||
for (Path htmlFile : htmlFiles) {
|
||||
if (htmlFile.getFileName().toString().equals("index.html")) {
|
||||
return htmlFile;
|
||||
}
|
||||
}
|
||||
|
||||
return htmlFiles.get(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +1,153 @@
|
||||
package stirling.software.SPDF.utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GeneralUtils {
|
||||
|
||||
public static Long convertSizeToBytes(String sizeStr) {
|
||||
if (sizeStr == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
sizeStr = sizeStr.trim().toUpperCase();
|
||||
try {
|
||||
if (sizeStr.endsWith("KB")) {
|
||||
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024);
|
||||
} else if (sizeStr.endsWith("MB")) {
|
||||
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024);
|
||||
} else if (sizeStr.endsWith("GB")) {
|
||||
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024 * 1024);
|
||||
} else if (sizeStr.endsWith("B")) {
|
||||
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
|
||||
} else {
|
||||
// Input string does not have a valid format, handle this case
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// The numeric part of the input string cannot be parsed, handle this case
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<Integer> parsePageList(String[] pageOrderArr, int totalPages) {
|
||||
List<Integer> newPageOrder = new ArrayList<>();
|
||||
|
||||
// loop through the page order array
|
||||
for (String element : pageOrderArr) {
|
||||
// check if the element contains a range of pages
|
||||
if (element.matches("\\d*n\\+?-?\\d*|\\d*\\+?n")) {
|
||||
// Handle page order as a function
|
||||
int coefficient = 0;
|
||||
int constant = 0;
|
||||
boolean coefficientExists = false;
|
||||
boolean constantExists = false;
|
||||
|
||||
if (element.contains("n")) {
|
||||
String[] parts = element.split("n");
|
||||
if (!parts[0].equals("") && parts[0] != null) {
|
||||
coefficient = Integer.parseInt(parts[0]);
|
||||
coefficientExists = true;
|
||||
}
|
||||
if (parts.length > 1 && !parts[1].equals("") && parts[1] != null) {
|
||||
constant = Integer.parseInt(parts[1]);
|
||||
constantExists = true;
|
||||
}
|
||||
} else if (element.contains("+")) {
|
||||
constant = Integer.parseInt(element.replace("+", ""));
|
||||
constantExists = true;
|
||||
}
|
||||
|
||||
for (int i = 1; i <= totalPages; i++) {
|
||||
int pageNum = coefficientExists ? coefficient * i : i;
|
||||
pageNum += constantExists ? constant : 0;
|
||||
|
||||
if (pageNum <= totalPages && pageNum > 0) {
|
||||
newPageOrder.add(pageNum - 1);
|
||||
}
|
||||
}
|
||||
} else if (element.contains("-")) {
|
||||
// split the range into start and end page
|
||||
String[] range = element.split("-");
|
||||
int start = Integer.parseInt(range[0]);
|
||||
int end = Integer.parseInt(range[1]);
|
||||
// check if the end page is greater than total pages
|
||||
if (end > totalPages) {
|
||||
end = totalPages;
|
||||
}
|
||||
// loop through the range of pages
|
||||
for (int j = start; j <= end; j++) {
|
||||
// print the current index
|
||||
newPageOrder.add(j - 1);
|
||||
}
|
||||
} else {
|
||||
// if the element is a single page
|
||||
newPageOrder.add(Integer.parseInt(element) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
return newPageOrder;
|
||||
}
|
||||
}
|
||||
package stirling.software.SPDF.utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GeneralUtils {
|
||||
|
||||
public static void deleteDirectory(Path path) throws IOException {
|
||||
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
Files.delete(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
Files.delete(dir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static String convertToFileName(String name) {
|
||||
String safeName = name.replaceAll("[^a-zA-Z0-9]", "_");
|
||||
if (safeName.length() > 50) {
|
||||
safeName = safeName.substring(0, 50);
|
||||
}
|
||||
return safeName;
|
||||
}
|
||||
|
||||
|
||||
public static boolean isValidURL(String urlStr) {
|
||||
try {
|
||||
new URL(urlStr);
|
||||
return true;
|
||||
} catch (MalformedURLException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static Long convertSizeToBytes(String sizeStr) {
|
||||
if (sizeStr == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
sizeStr = sizeStr.trim().toUpperCase();
|
||||
try {
|
||||
if (sizeStr.endsWith("KB")) {
|
||||
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024);
|
||||
} else if (sizeStr.endsWith("MB")) {
|
||||
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024);
|
||||
} else if (sizeStr.endsWith("GB")) {
|
||||
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024 * 1024);
|
||||
} else if (sizeStr.endsWith("B")) {
|
||||
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
|
||||
} else {
|
||||
// Input string does not have a valid format, handle this case
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// The numeric part of the input string cannot be parsed, handle this case
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<Integer> parsePageList(String[] pageOrderArr, int totalPages) {
|
||||
List<Integer> newPageOrder = new ArrayList<>();
|
||||
|
||||
// loop through the page order array
|
||||
for (String element : pageOrderArr) {
|
||||
if (element.equalsIgnoreCase("all")) {
|
||||
for (int i = 0; i < totalPages; i++) {
|
||||
newPageOrder.add(i);
|
||||
}
|
||||
// As all pages are already added, no need to check further
|
||||
break;
|
||||
}
|
||||
else if (element.matches("\\d*n\\+?-?\\d*|\\d*\\+?n")) {
|
||||
// Handle page order as a function
|
||||
int coefficient = 0;
|
||||
int constant = 0;
|
||||
boolean coefficientExists = false;
|
||||
boolean constantExists = false;
|
||||
|
||||
if (element.contains("n")) {
|
||||
String[] parts = element.split("n");
|
||||
if (!parts[0].equals("") && parts[0] != null) {
|
||||
coefficient = Integer.parseInt(parts[0]);
|
||||
coefficientExists = true;
|
||||
}
|
||||
if (parts.length > 1 && !parts[1].equals("") && parts[1] != null) {
|
||||
constant = Integer.parseInt(parts[1]);
|
||||
constantExists = true;
|
||||
}
|
||||
} else if (element.contains("+")) {
|
||||
constant = Integer.parseInt(element.replace("+", ""));
|
||||
constantExists = true;
|
||||
}
|
||||
|
||||
for (int i = 1; i <= totalPages; i++) {
|
||||
int pageNum = coefficientExists ? coefficient * i : i;
|
||||
pageNum += constantExists ? constant : 0;
|
||||
|
||||
if (pageNum <= totalPages && pageNum > 0) {
|
||||
newPageOrder.add(pageNum - 1);
|
||||
}
|
||||
}
|
||||
} else if (element.contains("-")) {
|
||||
// split the range into start and end page
|
||||
String[] range = element.split("-");
|
||||
int start = Integer.parseInt(range[0]);
|
||||
int end = Integer.parseInt(range[1]);
|
||||
// check if the end page is greater than total pages
|
||||
if (end > totalPages) {
|
||||
end = totalPages;
|
||||
}
|
||||
// loop through the range of pages
|
||||
for (int j = start; j <= end; j++) {
|
||||
// print the current index
|
||||
newPageOrder.add(j - 1);
|
||||
}
|
||||
} else {
|
||||
// if the element is a single page
|
||||
newPageOrder.add(Integer.parseInt(element) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
return newPageOrder;
|
||||
}
|
||||
public static boolean createDir(String path) {
|
||||
Path folder = Paths.get(path);
|
||||
if (!Files.exists(folder)) {
|
||||
try {
|
||||
Files.createDirectories(folder);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user