mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2024-11-18 21:30:10 +01:00
Merge branch 'main' into on_hover-pagenumber-display#527
This commit is contained in:
commit
dfee149da0
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
@ -0,0 +1,2 @@
|
||||
# Formatting
|
||||
5f771b785130154ed47952635b7acef371ffe0ec
|
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@ -9,3 +9,7 @@ updates:
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/" # Location of Dockerfile
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
34
.github/workflows/build.yml
vendored
Normal file
34
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
name: "Build repo"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
- uses: gradle/gradle-build-action@v2.4.2
|
||||
with:
|
||||
gradle-version: 7.6
|
||||
arguments: build --no-build-cache
|
55
.github/workflows/codeql.yml
vendored
55
.github/workflows/codeql.yml
vendored
@ -1,55 +0,0 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
|
||||
name: "Build repo"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '15 12 * * 1'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
# - name: Initialize CodeQL
|
||||
# uses: github/codeql-action/init@v2
|
||||
# with:
|
||||
# languages: java
|
||||
|
||||
- uses: gradle/gradle-build-action@v2.4.2
|
||||
with:
|
||||
gradle-version: 7.6
|
||||
arguments: assemble --no-build-cache
|
||||
|
||||
#- name: Perform CodeQL analysis
|
||||
# uses: github/codeql-action/analyze@v2
|
56
.github/workflows/test.yml
vendored
Normal file
56
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
name: Docker Compose Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/**'
|
||||
- '**.gradle'
|
||||
- 'exampleYmlFiles/**'
|
||||
- 'Dockerfile'
|
||||
- 'Dockerfile**'
|
||||
paths-ignore:
|
||||
- 'src/main/java/resources/messages*'
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Run Docker Compose Tests
|
||||
run: |
|
||||
chmod +x ./gradlew
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: echo "::set-output name=versionNumber::$(./gradlew printVersion --quiet | tail -1)"
|
||||
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ steps.versionNumber.outputs.versionNumber }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Install Docker Compose
|
||||
run: |
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/v2.6.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
|
||||
|
||||
- name: Run Docker Compose Tests
|
||||
run: |
|
||||
chmod +x ./test.sh
|
||||
./test.sh
|
@ -1,4 +1,4 @@
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/Frooodle/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1>
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1>
|
||||
</p>
|
||||
|
||||
|
||||
@ -8,9 +8,9 @@ Fork Stirling-PDF and make a new branch out of Main
|
||||
|
||||
Then add reference to the language in the navbar by adding a new language entry to the dropdown
|
||||
|
||||
https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html
|
||||
https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html
|
||||
and add a flag svg file to
|
||||
https://github.com/Frooodle/Stirling-PDF/tree/main/src/main/resources/static/images/flags
|
||||
https://github.com/Stirling-Tools/Stirling-PDF/tree/main/src/main/resources/static/images/flags
|
||||
Any SVG flags are fine, i got most of mine from [here](https://flagicons.lipis.dev/)
|
||||
If your language isnt represented by a flag just find whichever closely matches it, such as for Arabic i chose Saudi Arabia
|
||||
|
||||
@ -25,7 +25,7 @@ The data-language-code is the code used to reference the file in the next step.
|
||||
|
||||
Start by copying the existing english property file
|
||||
|
||||
[https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties](https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties)
|
||||
[https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties)
|
||||
|
||||
Copy and rename it to messages_{your data-language-code here}.properties, in the polish example you would set the name to messages_pl_PL.properties
|
||||
|
||||
|
@ -109,7 +109,7 @@ pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
|
||||
|
||||
```bash
|
||||
cd ~/.git &&\
|
||||
git clone https://github.com/Frooodle/Stirling-PDF.git &&\
|
||||
git clone https://github.com/Stirling-Tools/Stirling-PDF.git &&\
|
||||
cd Stirling-PDF &&\
|
||||
chmod +x ./gradlew &&\
|
||||
./gradlew build
|
||||
|
26
README.md
26
README.md
@ -1,14 +1,14 @@
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/Frooodle/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1>
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1>
|
||||
</p>
|
||||
|
||||
[![Docker Pulls](https://img.shields.io/docker/pulls/frooodle/s-pdf)](https://hub.docker.com/r/frooodle/s-pdf)
|
||||
[![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/Cn8pWhQRxZ)
|
||||
[![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Frooodle/Stirling-PDF/)
|
||||
[![GitHub Repo stars](https://img.shields.io/github/stars/frooodle/stirling-pdf?style=social)](https://github.com/Frooodle/stirling-pdf)
|
||||
[![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/)
|
||||
[![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](https://github.com/Stirling-Tools/stirling-pdf)
|
||||
[![Paypal Donate](https://img.shields.io/badge/Paypal%20Donate-yellow?style=flat&logo=paypal)](https://www.paypal.com/paypalme/froodleplex)
|
||||
[![Github Sponser](https://img.shields.io/badge/Github%20Sponsor-yellow?style=flat&logo=github)](https://github.com/sponsors/Frooodle)
|
||||
|
||||
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Frooodle/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
||||
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
||||
|
||||
This is a powerful locally hosted web based PDF manipulation tool using docker that allows you to perform various operations on PDF files, such as splitting merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application started as a 100% ChatGPT-made application and has evolved to include a wide range of features to handle all your PDF needs.
|
||||
|
||||
@ -22,10 +22,10 @@ Please feel free to submit feature requests or report bugs either through GitHub
|
||||
|
||||
## Features
|
||||
- Dark mode support.
|
||||
- Custom download options (see [here](https://github.com/Frooodle/Stirling-PDF/blob/main/images/settings.png) for example)
|
||||
- Custom download options (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/images/settings.png) for example)
|
||||
- Parallel file processing and downloads
|
||||
- API for integration with external scripts
|
||||
- Optional Login and Authentication support (see [here](https://github.com/Frooodle/Stirling-PDF/tree/main#login-authentication) for documentation)
|
||||
- Optional Login and Authentication support (see [here](https://github.com/Stirling-Tools/Stirling-PDF/tree/main#login-authentication) for documentation)
|
||||
|
||||
|
||||
## **PDF Features**
|
||||
@ -80,7 +80,7 @@ Please feel free to submit feature requests or report bugs either through GitHub
|
||||
- Get all information on a PDF to view or export as JSON.
|
||||
|
||||
|
||||
For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Frooodle/Stirling-PDF/blob/main/Endpoint-groups.md)
|
||||
For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
|
||||
Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) hosted by the team at adminforge.de
|
||||
|
||||
## Technologies used
|
||||
@ -96,13 +96,13 @@ Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) h
|
||||
## How to use
|
||||
|
||||
### Locally
|
||||
Please view https://github.com/Frooodle/Stirling-PDF/blob/main/LocalRunGuide.md
|
||||
Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LocalRunGuide.md
|
||||
|
||||
### Docker / Podman
|
||||
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)
|
||||
To see what the different versions offer please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md)
|
||||
For people that don't mind about space optimization just use the latest tag.
|
||||
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest?label=Stirling-PDF%20Full)
|
||||
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest-lite?label=Stirling-PDF%20Lite)
|
||||
@ -144,7 +144,7 @@ services:
|
||||
Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "podman".
|
||||
|
||||
## Enable OCR/Compression feature
|
||||
Please view https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md
|
||||
Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md
|
||||
|
||||
## Want to add your own language?
|
||||
Stirling PDF currently supports 21!
|
||||
@ -172,7 +172,7 @@ Stirling PDF currently supports 21!
|
||||
- Hindi (हिंदी) (hi_IN)
|
||||
|
||||
If you want to add your own language to Stirling-PDF please refer
|
||||
https://github.com/Frooodle/Stirling-PDF/blob/main/HowToAddNewLanguage.md
|
||||
https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md
|
||||
|
||||
And please create a PR to merge it back in so others can use it!
|
||||
|
||||
@ -224,7 +224,7 @@ metrics:
|
||||
enabled: true # 'true' to enable Info APIs endpoints (view http://localhost:8080/swagger-ui/index.html#/API to learn more), 'false' to disable
|
||||
```
|
||||
### Extra notes
|
||||
- Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Frooodle/Stirling-PDF/blob/main/Endpoint-groups.md)
|
||||
- Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
|
||||
- customStaticFilePath. Customise static files such as the app logo by placing files in the /customFiles/static/ directory. An example of customising app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF
|
||||
|
||||
### Environment only parameters
|
||||
@ -234,7 +234,7 @@ metrics:
|
||||
|
||||
## 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 (Or by following the API button in your settings of Stirling-PDF)
|
||||
[here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF)
|
||||
|
||||
|
||||
## Login authentication
|
||||
|
32
build.gradle
32
build.gradle
@ -1,10 +1,11 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '3.1.2'
|
||||
id 'org.springframework.boot' version '3.2.1'
|
||||
id 'io.spring.dependency-management' version '1.1.3'
|
||||
id 'org.springdoc.openapi-gradle-plugin' version '1.8.0'
|
||||
id "io.swagger.swaggerhub" version "1.3.2"
|
||||
id 'edu.sc.seis.launch4j' version '3.0.5'
|
||||
id 'com.diffplug.spotless' version '6.23.3'
|
||||
}
|
||||
|
||||
group = 'stirling.software'
|
||||
@ -61,21 +62,37 @@ launch4j {
|
||||
messagesInstanceAlreadyExists="Stirling-PDF is already running."
|
||||
}
|
||||
|
||||
spotless {
|
||||
java {
|
||||
target project.fileTree('src/main/java')
|
||||
|
||||
googleJavaFormat('1.19.1').aosp().reorderImports(false)
|
||||
|
||||
importOrder('java', 'javax', 'org', 'com', 'net', 'io')
|
||||
toggleOffOn()
|
||||
trimTrailingWhitespace()
|
||||
indentWithSpaces()
|
||||
endWithNewline()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
//security updates
|
||||
implementation 'ch.qos.logback:logback-classic:1.4.14'
|
||||
implementation 'ch.qos.logback:logback-core:1.4.14'
|
||||
implementation 'org.springframework:spring-webmvc:6.0.15'
|
||||
implementation 'org.springframework:spring-webmvc:6.1.2'
|
||||
|
||||
implementation 'org.yaml:snakeyaml:2.1'
|
||||
implementation 'org.yaml:snakeyaml:2.2'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web:3.2.1'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.2.1'
|
||||
|
||||
if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security:3.2.1'
|
||||
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE'
|
||||
implementation "org.springframework.boot:spring-boot-starter-data-jpa"
|
||||
implementation "com.h2database:h2"
|
||||
implementation "org.springframework.boot:spring-boot-starter-data-jpa:3.2.1"
|
||||
|
||||
//2.2.x requires rebuild of DB file.. need migration path
|
||||
implementation "com.h2database:h2:2.1.214"
|
||||
}
|
||||
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.2.1'
|
||||
@ -107,7 +124,7 @@ dependencies {
|
||||
//general PDF
|
||||
|
||||
// https://mvnrepository.com/artifact/com.opencsv/opencsv
|
||||
implementation ('com.opencsv:opencsv:5.7.1') {
|
||||
implementation ('com.opencsv:opencsv:5.9') {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
}
|
||||
|
||||
@ -134,6 +151,9 @@ dependencies {
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.28'
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
dependsOn 'spotlessApply'
|
||||
}
|
||||
|
||||
task writeVersion {
|
||||
def propsFile = file('src/main/resources/version.properties')
|
||||
|
@ -1,15 +1,15 @@
|
||||
apiVersion: v2
|
||||
appVersion: 0.14.2
|
||||
description: locally hosted web application that allows you to perform various operations on PDF files
|
||||
home: https://github.com/Frooodle/Stirling-PDF
|
||||
home: https://github.com/Stirling-Tools/Stirling-PDF
|
||||
keywords:
|
||||
- stirling-pdf
|
||||
- helm
|
||||
- charts repo
|
||||
maintainers:
|
||||
- name: Frooodle
|
||||
url: https://github.com/Frooodle/Stirling-PDF
|
||||
- name: Stirling-Tools
|
||||
url: https://github.com/Stirling-Tools/Stirling-PDF
|
||||
name: stirling-pdf-chart
|
||||
sources:
|
||||
- https://github.com/Frooodle/Stirling-PDF
|
||||
- https://github.com/Stirling-Tools/Stirling-PDF
|
||||
version: 1.0.0
|
||||
|
31
exampleYmlFiles/docker-compose-latest-lite-security.yml
Normal file
31
exampleYmlFiles/docker-compose-latest-lite-security.yml
Normal file
@ -0,0 +1,31 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Lite-Security
|
||||
image: frooodle/s-pdf:latest-lite
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 2G
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 16
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- /stirling/latest/data:/usr/share/tesseract-ocr/5/tessdata:rw
|
||||
- /stirling/latest/config:/configs:rw
|
||||
- /stirling/latest/logs:/logs:rw
|
||||
environment:
|
||||
DOCKER_ENABLE_SECURITY: "true"
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
SYSTEM_DEFAULTLOCALE: en_US
|
||||
UI_APPNAME: Stirling-PDF-Lite
|
||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Lite Latest with Security
|
||||
UI_APPNAMENAVBAR: Stirling-PDF-Lite Latest
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||
restart: on-failure:5
|
30
exampleYmlFiles/docker-compose-latest-lite.yml
Normal file
30
exampleYmlFiles/docker-compose-latest-lite.yml
Normal file
@ -0,0 +1,30 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Lite
|
||||
image: frooodle/s-pdf:latest-lite
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 2G
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 16
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- /stirling/latest/config:/configs:rw
|
||||
- /stirling/latest/logs:/logs:rw
|
||||
environment:
|
||||
DOCKER_ENABLE_SECURITY: "false"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en_US
|
||||
UI_APPNAME: Stirling-PDF-Lite
|
||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Lite Latest
|
||||
UI_APPNAMENAVBAR: Stirling-PDF-Lite Latest
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||
restart: on-failure:5
|
31
exampleYmlFiles/docker-compose-latest-security.yml
Normal file
31
exampleYmlFiles/docker-compose-latest-security.yml
Normal file
@ -0,0 +1,31 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Security
|
||||
image: frooodle/s-pdf:latest
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 4G
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 16
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- /stirling/latest/data:/usr/share/tesseract-ocr/5/tessdata:rw
|
||||
- /stirling/latest/config:/configs:rw
|
||||
- /stirling/latest/logs:/logs:rw
|
||||
environment:
|
||||
DOCKER_ENABLE_SECURITY: "true"
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
SYSTEM_DEFAULTLOCALE: en_US
|
||||
UI_APPNAME: Stirling-PDF
|
||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest with Security
|
||||
UI_APPNAMENAVBAR: Stirling-PDF Latest
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||
restart: on-failure:5
|
@ -0,0 +1,31 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Ultra-Lite-Security
|
||||
image: frooodle/s-pdf:latest-ultra-lite
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1G
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 16
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- /stirling/latest/data:/usr/share/tesseract-ocr/5/tessdata:rw
|
||||
- /stirling/latest/config:/configs:rw
|
||||
- /stirling/latest/logs:/logs:rw
|
||||
environment:
|
||||
DOCKER_ENABLE_SECURITY: "true"
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
SYSTEM_DEFAULTLOCALE: en_US
|
||||
UI_APPNAME: Stirling-PDF-Lite
|
||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Lite Latest with Security
|
||||
UI_APPNAMENAVBAR: Stirling-PDF-Lite Latest
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||
restart: on-failure:5
|
30
exampleYmlFiles/docker-compose-latest-ultra-lite.yml
Normal file
30
exampleYmlFiles/docker-compose-latest-ultra-lite.yml
Normal file
@ -0,0 +1,30 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Ultra-Lite
|
||||
image: frooodle/s-pdf:latest-ultra-lite
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1G
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 16
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- /stirling/latest/config:/configs:rw
|
||||
- /stirling/latest/logs:/logs:rw
|
||||
environment:
|
||||
DOCKER_ENABLE_SECURITY: "false"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en_US
|
||||
UI_APPNAME: Stirling-PDF-Ultra-lite
|
||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Ultra-lite Latest
|
||||
UI_APPNAMENAVBAR: Stirling-PDF-Ultra-lite Latest
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||
restart: on-failure:5
|
31
exampleYmlFiles/docker-compose-latest.yml
Normal file
31
exampleYmlFiles/docker-compose-latest.yml
Normal file
@ -0,0 +1,31 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF
|
||||
image: frooodle/s-pdf:latest
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 4G
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 16
|
||||
ports:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- /stirling/latest/data:/usr/share/tesseract-ocr/5/tessdata:rw
|
||||
- /stirling/latest/config:/configs:rw
|
||||
- /stirling/latest/logs:/logs:rw
|
||||
environment:
|
||||
DOCKER_ENABLE_SECURITY: "false"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en_US
|
||||
UI_APPNAME: Stirling-PDF
|
||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest
|
||||
UI_APPNAMENAVBAR: Stirling-PDF Latest
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||
restart: on-failure:5
|
@ -2,13 +2,13 @@ echo "Running Stirling PDF with DOCKER_ENABLE_SECURITY=${DOCKER_ENABLE_SECURITY}
|
||||
# Check for DOCKER_ENABLE_SECURITY and download the appropriate JAR if required
|
||||
if [ "$DOCKER_ENABLE_SECURITY" = "true" ] && [ "$VERSION_TAG" != "alpha" ]; then
|
||||
if [ ! -f app-security.jar ]; then
|
||||
echo "Trying to download from: https://github.com/Frooodle/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar"
|
||||
curl -L -o app-security.jar https://github.com/Frooodle/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar
|
||||
echo "Trying to download from: https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar"
|
||||
curl -L -o app-security.jar https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar
|
||||
|
||||
# If the first download attempt failed, try with the 'v' prefix
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Trying to download from: https://github.com/Frooodle/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar"
|
||||
curl -L -o app-security.jar https://github.com/Frooodle/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar
|
||||
echo "Trying to download from: https://github.com/Stirling-Tools/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar"
|
||||
curl -L -o app-security.jar https://github.com/Stirling-Tools/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar
|
||||
fi
|
||||
|
||||
if [ $? -eq 0 ]; then # checks if curl was successful
|
||||
|
@ -22,14 +22,14 @@ public class LibreOfficeListener {
|
||||
|
||||
private Process process;
|
||||
|
||||
private LibreOfficeListener() {
|
||||
}
|
||||
private LibreOfficeListener() {}
|
||||
|
||||
private boolean isListenerRunning() {
|
||||
try {
|
||||
System.out.println("waiting for listener to start");
|
||||
Socket socket = new Socket();
|
||||
socket.connect(new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second
|
||||
socket.connect(
|
||||
new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second
|
||||
socket.close();
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
@ -49,7 +49,8 @@ public class LibreOfficeListener {
|
||||
|
||||
// Start a background thread to monitor the activity timeout
|
||||
executorService = Executors.newSingleThreadExecutor();
|
||||
executorService.submit(() -> {
|
||||
executorService.submit(
|
||||
() -> {
|
||||
while (true) {
|
||||
long idleTime = System.currentTimeMillis() - lastActivityTime;
|
||||
if (idleTime >= ACTIVITY_TIMEOUT) {
|
||||
@ -92,5 +93,4 @@ public class LibreOfficeListener {
|
||||
process.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -13,13 +13,12 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import stirling.software.SPDF.config.ConfigInitializer;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
@SpringBootApplication
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
public class SPdfApplication {
|
||||
|
||||
@Autowired
|
||||
private Environment env;
|
||||
@Autowired private Environment env;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
@ -47,9 +46,12 @@ public class SPdfApplication {
|
||||
SpringApplication app = new SpringApplication(SPdfApplication.class);
|
||||
app.addInitializers(new ConfigInitializer());
|
||||
if (Files.exists(Paths.get("configs/settings.yml"))) {
|
||||
app.setDefaultProperties(Collections.singletonMap("spring.config.additional-location", "file:configs/settings.yml"));
|
||||
app.setDefaultProperties(
|
||||
Collections.singletonMap(
|
||||
"spring.config.additional-location", "file:configs/settings.yml"));
|
||||
} else {
|
||||
System.out.println("External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
|
||||
System.out.println(
|
||||
"External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
|
||||
}
|
||||
app.run(args);
|
||||
|
||||
|
@ -5,12 +5,11 @@ 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;
|
||||
@Autowired ApplicationProperties applicationProperties;
|
||||
|
||||
@Bean(name = "loginEnabled")
|
||||
public boolean loginEnabled() {
|
||||
@ -31,28 +30,31 @@ public class AppConfig {
|
||||
|
||||
@Bean(name = "homeText")
|
||||
public String homeText() {
|
||||
return (applicationProperties.getUi().getHomeDescription() != null) ? applicationProperties.getUi().getHomeDescription() : "null";
|
||||
return (applicationProperties.getUi().getHomeDescription() != null)
|
||||
? applicationProperties.getUi().getHomeDescription()
|
||||
: "null";
|
||||
}
|
||||
|
||||
|
||||
@Bean(name = "navBarText")
|
||||
public String navBarText() {
|
||||
String defaultNavBar = applicationProperties.getUi().getAppNameNavbar() != null ? applicationProperties.getUi().getAppNameNavbar() : applicationProperties.getUi().getAppName();
|
||||
String defaultNavBar =
|
||||
applicationProperties.getUi().getAppNameNavbar() != null
|
||||
? applicationProperties.getUi().getAppNameNavbar()
|
||||
: applicationProperties.getUi().getAppName();
|
||||
return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF";
|
||||
}
|
||||
|
||||
@Bean(name = "enableAlphaFunctionality")
|
||||
public boolean enableAlphaFunctionality() {
|
||||
return applicationProperties.getSystem().getEnableAlphaFunctionality() != null ? applicationProperties.getSystem().getEnableAlphaFunctionality() : false;
|
||||
return applicationProperties.getSystem().getEnableAlphaFunctionality() != null
|
||||
? applicationProperties.getSystem().getEnableAlphaFunctionality()
|
||||
: false;
|
||||
}
|
||||
|
||||
@Bean(name = "rateLimit")
|
||||
public boolean rateLimit() {
|
||||
String appName = System.getProperty("rateLimit");
|
||||
if (appName == null)
|
||||
appName = System.getenv("rateLimit");
|
||||
if (appName == null) appName = System.getenv("rateLimit");
|
||||
return (appName != null) ? Boolean.valueOf(appName) : false;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -16,8 +16,7 @@ import stirling.software.SPDF.model.ApplicationProperties;
|
||||
@Configuration
|
||||
public class Beans implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
ApplicationProperties applicationProperties;
|
||||
@Autowired ApplicationProperties applicationProperties;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
@ -36,9 +35,9 @@ public class Beans implements WebMvcConfigurer {
|
||||
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
|
||||
Locale defaultLocale =
|
||||
Locale.UK; // Fallback to UK locale if environment variable is not set
|
||||
|
||||
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
|
||||
Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
|
||||
@ -53,7 +52,8 @@ public class Beans implements WebMvcConfigurer {
|
||||
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
|
||||
defaultLocale = tempLocale;
|
||||
} else {
|
||||
System.err.println("Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK.");
|
||||
System.err.println(
|
||||
"Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -61,5 +61,4 @@ public class Beans implements WebMvcConfigurer {
|
||||
slr.setDefaultLocale(defaultLocale);
|
||||
return slr;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -13,11 +13,13 @@ 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", "messageType");
|
||||
|
||||
private static final List<String> ALLOWED_PARAMS =
|
||||
Arrays.asList(
|
||||
"lang", "endpoint", "endpoints", "logout", "error", "file", "messageType");
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||
public boolean preHandle(
|
||||
HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||
throws Exception {
|
||||
String queryString = request.getQueryString();
|
||||
if (queryString != null && !queryString.isEmpty()) {
|
||||
@ -57,12 +59,16 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
|
||||
ModelAndView modelAndView) {
|
||||
}
|
||||
public void postHandle(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
Object handler,
|
||||
ModelAndView modelAndView) {}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
|
||||
Exception ex) {
|
||||
}
|
||||
public void afterCompletion(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
Object handler,
|
||||
Exception ex) {}
|
||||
}
|
||||
|
@ -19,7 +19,8 @@ import java.util.stream.Collectors;
|
||||
import org.springframework.context.ApplicationContextInitializer;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
public class ConfigInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
||||
public class ConfigInitializer
|
||||
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
||||
|
||||
@Override
|
||||
public void initialize(ConfigurableApplicationContext applicationContext) {
|
||||
@ -40,18 +41,23 @@ public class ConfigInitializer implements ApplicationContextInitializer<Configur
|
||||
Files.createDirectories(destPath.getParent());
|
||||
|
||||
// Copy the resource from classpath to the external directory
|
||||
try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
||||
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");
|
||||
throw new FileNotFoundException(
|
||||
"Resource file not found: settings.yml.template");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If user file exists, we need to merge it with the template from the classpath
|
||||
List<String> templateLines;
|
||||
try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
||||
templateLines = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)).lines()
|
||||
try (InputStream in =
|
||||
getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
||||
templateLines =
|
||||
new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
|
||||
.lines()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@ -59,14 +65,16 @@ public class ConfigInitializer implements ApplicationContextInitializer<Configur
|
||||
}
|
||||
}
|
||||
|
||||
public void mergeYamlFiles(List<String> templateLines, Path userFilePath, Path outputPath) throws IOException {
|
||||
public void mergeYamlFiles(List<String> templateLines, Path userFilePath, Path outputPath)
|
||||
throws IOException {
|
||||
List<String> userLines = Files.readAllLines(userFilePath);
|
||||
List<String> mergedLines = new ArrayList<>();
|
||||
boolean insideAutoGenerated = false;
|
||||
boolean beforeFirstKey = true;
|
||||
|
||||
Function<String, Boolean> isCommented = line -> line.trim().startsWith("#");
|
||||
Function<String, String> extractKey = line -> {
|
||||
Function<String, String> extractKey =
|
||||
line -> {
|
||||
String[] parts = line.split(":");
|
||||
return parts.length > 0 ? parts[0].trim().replace("#", "").trim() : "";
|
||||
};
|
||||
@ -92,21 +100,26 @@ public class ConfigInitializer implements ApplicationContextInitializer<Configur
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!key.isEmpty())
|
||||
beforeFirstKey = false;
|
||||
if (!key.isEmpty()) beforeFirstKey = false;
|
||||
|
||||
if (userKeys.contains(key)) {
|
||||
// If user has any version (commented or uncommented) of this key, skip the
|
||||
// template line
|
||||
Optional<String> userValue = userLines.stream()
|
||||
.filter(l -> extractKey.apply(l).equalsIgnoreCase(key) && !isCommented.apply(l)).findFirst();
|
||||
if (userValue.isPresent())
|
||||
mergedLines.add(userValue.get());
|
||||
Optional<String> userValue =
|
||||
userLines.stream()
|
||||
.filter(
|
||||
l ->
|
||||
extractKey.apply(l).equalsIgnoreCase(key)
|
||||
&& !isCommented.apply(l))
|
||||
.findFirst();
|
||||
if (userValue.isPresent()) mergedLines.add(userValue.get());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isCommented.apply(line) || line.trim().isEmpty() || !userKeys.contains(key)) {
|
||||
mergedLines.add(line); // If line is commented, empty or key not present in user's file, retain the
|
||||
mergedLines.add(
|
||||
line); // If line is commented, empty or key not present in user's file,
|
||||
// retain the
|
||||
// template line
|
||||
continue;
|
||||
}
|
||||
@ -116,7 +129,9 @@ public class ConfigInitializer implements ApplicationContextInitializer<Configur
|
||||
// template
|
||||
for (String userLine : userLines) {
|
||||
String userKey = extractKey.apply(userLine);
|
||||
boolean isPresentInTemplate = templateLines.stream().map(extractKey)
|
||||
boolean isPresentInTemplate =
|
||||
templateLines.stream()
|
||||
.map(extractKey)
|
||||
.anyMatch(templateKey -> templateKey.equalsIgnoreCase(userKey));
|
||||
if (!isPresentInTemplate && !isCommented.apply(userLine)) {
|
||||
mergedLines.add(userLine);
|
||||
@ -125,5 +140,4 @@ public class ConfigInitializer implements ApplicationContextInitializer<Configur
|
||||
|
||||
Files.write(outputPath, mergedLines, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
}
|
@ -12,6 +12,7 @@ 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);
|
||||
@ -85,7 +86,6 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("PageOps", "overlay-pdf");
|
||||
addEndpointToGroup("PageOps", "split-pdf-by-sections");
|
||||
|
||||
|
||||
// Adding endpoints to "Convert" group
|
||||
addEndpointToGroup("Convert", "pdf-to-img");
|
||||
addEndpointToGroup("Convert", "img-to-pdf");
|
||||
@ -102,7 +102,6 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Convert", "markdown-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-csv");
|
||||
|
||||
|
||||
// Adding endpoints to "Security" group
|
||||
addEndpointToGroup("Security", "add-password");
|
||||
addEndpointToGroup("Security", "remove-password");
|
||||
@ -112,7 +111,6 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Security", "sanitize-pdf");
|
||||
addEndpointToGroup("Security", "auto-redact");
|
||||
|
||||
|
||||
// Adding endpoints to "Other" group
|
||||
addEndpointToGroup("Other", "ocr-pdf");
|
||||
addEndpointToGroup("Other", "add-image");
|
||||
@ -131,8 +129,6 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Other", "get-info-on-pdf");
|
||||
addEndpointToGroup("Other", "show-javascript");
|
||||
|
||||
|
||||
|
||||
// CLI
|
||||
addEndpointToGroup("CLI", "compress-pdf");
|
||||
addEndpointToGroup("CLI", "extract-image-scans");
|
||||
@ -150,7 +146,6 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("CLI", "html-to-pdf");
|
||||
addEndpointToGroup("CLI", "url-to-pdf");
|
||||
|
||||
|
||||
// python
|
||||
addEndpointToGroup("Python", "extract-image-scans");
|
||||
addEndpointToGroup("Python", "remove-blanks");
|
||||
@ -171,7 +166,6 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-html");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-xml");
|
||||
|
||||
|
||||
// OCRmyPDF
|
||||
addEndpointToGroup("OCRmyPDF", "compress-pdf");
|
||||
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
|
||||
@ -216,8 +210,6 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Javascript", "sign");
|
||||
addEndpointToGroup("Javascript", "compare");
|
||||
addEndpointToGroup("Javascript", "adjust-contrast");
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void processEnvironmentConfigs() {
|
||||
@ -236,6 +228,4 @@ public class EndpointConfiguration {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -10,11 +10,11 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
@Component
|
||||
public class EndpointInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Autowired
|
||||
private EndpointConfiguration endpointConfiguration;
|
||||
@Autowired private EndpointConfiguration endpointConfiguration;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||
public boolean preHandle(
|
||||
HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||
throws Exception {
|
||||
String requestURI = request.getRequestURI();
|
||||
if (!endpointConfiguration.isEndpointEnabled(requestURI)) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
|
@ -8,6 +8,7 @@ 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;
|
||||
@ -24,20 +25,34 @@ public class MetricsFilter extends OncePerRequestFilter {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
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("/v1/api-docs") || uri.endsWith("robots.txt")
|
||||
|| uri.startsWith("/images") || uri.startsWith("/images")|| uri.endsWith(".png") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".map")
|
||||
|| uri.endsWith(".svg") || uri.endsWith(".js") || uri.contains("swagger")
|
||||
|| uri.startsWith("/api/v1/info") || uri.startsWith("/site.webmanifest") || uri.startsWith("/fonts") || uri.startsWith("/pdfjs") )) {
|
||||
if (!(uri.startsWith("/js")
|
||||
|| uri.startsWith("/v1/api-docs")
|
||||
|| uri.endsWith("robots.txt")
|
||||
|| uri.startsWith("/images")
|
||||
|| uri.startsWith("/images")
|
||||
|| uri.endsWith(".png")
|
||||
|| uri.endsWith(".ico")
|
||||
|| uri.endsWith(".css")
|
||||
|| uri.endsWith(".map")
|
||||
|| uri.endsWith(".svg")
|
||||
|| uri.endsWith(".js")
|
||||
|| uri.contains("swagger")
|
||||
|| uri.startsWith("/api/v1/info")
|
||||
|| uri.startsWith("/site.webmanifest")
|
||||
|| uri.startsWith("/fonts")
|
||||
|| uri.startsWith("/pdfjs"))) {
|
||||
|
||||
|
||||
|
||||
Counter counter = Counter.builder("http.requests").tag("uri", uri).tag("method", request.getMethod())
|
||||
Counter counter =
|
||||
Counter.builder("http.requests")
|
||||
.tag("uri", uri)
|
||||
.tag("method", request.getMethod())
|
||||
.register(meterRegistry);
|
||||
|
||||
counter.increment();
|
||||
@ -46,5 +61,4 @@ public class MetricsFilter extends OncePerRequestFilter {
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,13 +9,13 @@ import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
|
||||
@Configuration
|
||||
public class OpenApiConfig {
|
||||
|
||||
@Autowired
|
||||
ApplicationProperties applicationProperties;
|
||||
@Autowired ApplicationProperties applicationProperties;
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
@ -24,19 +24,30 @@ public class OpenApiConfig {
|
||||
version = "1.0.0"; // default version if all else fails
|
||||
}
|
||||
|
||||
SecurityScheme apiKeyScheme = new SecurityScheme().type(SecurityScheme.Type.APIKEY).in(SecurityScheme.In.HEADER)
|
||||
SecurityScheme apiKeyScheme =
|
||||
new SecurityScheme()
|
||||
.type(SecurityScheme.Type.APIKEY)
|
||||
.in(SecurityScheme.In.HEADER)
|
||||
.name("X-API-KEY");
|
||||
if (!applicationProperties.getSecurity().getEnableLogin()) {
|
||||
return new OpenAPI().components(new Components())
|
||||
.info(new Info().title("Stirling PDF API").version(version).description(
|
||||
return new OpenAPI()
|
||||
.components(new Components())
|
||||
.info(
|
||||
new Info()
|
||||
.title("Stirling PDF API")
|
||||
.version(version)
|
||||
.description(
|
||||
"API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."));
|
||||
} else {
|
||||
return new OpenAPI().components(new Components().addSecuritySchemes("apiKey", apiKeyScheme))
|
||||
.info(new Info().title("Stirling PDF API").version(version).description(
|
||||
return new OpenAPI()
|
||||
.components(new Components().addSecuritySchemes("apiKey", apiKeyScheme))
|
||||
.info(
|
||||
new Info()
|
||||
.title("Stirling PDF API")
|
||||
.version(version)
|
||||
.description(
|
||||
"API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."))
|
||||
.addSecurityItem(new SecurityRequirement().addList("apiKey"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.springframework.context.ApplicationListener;
|
||||
@ -17,4 +16,3 @@ public class StartupApplicationListener implements ApplicationListener<ContextRe
|
||||
startTime = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,8 +9,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private EndpointInterceptor endpointInterceptor;
|
||||
@Autowired private EndpointInterceptor endpointInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
|
@ -8,6 +8,7 @@ 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
|
||||
@ -18,6 +19,7 @@ public class YamlPropertySourceFactory implements PropertySourceFactory {
|
||||
|
||||
Properties properties = factory.getObject();
|
||||
|
||||
return new PropertiesPropertySource(encodedResource.getResource().getFilename(), properties);
|
||||
return new PropertiesPropertySource(
|
||||
encodedResource.getResource().getFilename(), properties);
|
||||
}
|
||||
}
|
@ -12,11 +12,11 @@ import org.springframework.stereotype.Component;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
@Component
|
||||
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||
|
||||
@Autowired
|
||||
private final LoginAttemptService loginAttemptService;
|
||||
@Autowired private final LoginAttemptService loginAttemptService;
|
||||
|
||||
@Autowired
|
||||
public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) {
|
||||
@ -24,7 +24,10 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
|
||||
public void onAuthenticationFailure(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AuthenticationException exception)
|
||||
throws IOException, ServletException {
|
||||
String ip = request.getRemoteAddr();
|
||||
logger.error("Failed login attempt from IP: " + ip);
|
||||
@ -41,7 +44,6 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
super.onAuthenticationFailure(request, response, exception);
|
||||
}
|
||||
}
|
||||
|
@ -15,21 +15,26 @@ import jakarta.servlet.http.HttpSession;
|
||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||
|
||||
@Component
|
||||
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||
public class CustomAuthenticationSuccessHandler
|
||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||
|
||||
@Autowired
|
||||
private LoginAttemptService loginAttemptService;
|
||||
@Autowired private LoginAttemptService loginAttemptService;
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
|
||||
public void onAuthenticationSuccess(
|
||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||
throws ServletException, IOException {
|
||||
String username = request.getParameter("username");
|
||||
loginAttemptService.loginSucceeded(username);
|
||||
|
||||
|
||||
// Get the saved request
|
||||
HttpSession session = request.getSession(false);
|
||||
SavedRequest savedRequest = session != null ? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST") : null;
|
||||
if (savedRequest != null && !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) {
|
||||
SavedRequest savedRequest =
|
||||
session != null
|
||||
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
|
||||
: null;
|
||||
if (savedRequest != null
|
||||
&& !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) {
|
||||
// Redirect to the original destination
|
||||
super.onAuthenticationSuccess(request, response, authentication);
|
||||
} else {
|
||||
@ -39,6 +44,4 @@ public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthent
|
||||
|
||||
// super.onAuthenticationSuccess(request, response, authentication);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -20,28 +20,33 @@ import stirling.software.SPDF.repository.UserRepository;
|
||||
@Service
|
||||
public class CustomUserDetailsService implements UserDetailsService {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
@Autowired private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private LoginAttemptService loginAttemptService;
|
||||
@Autowired private LoginAttemptService loginAttemptService;
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
User user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + username));
|
||||
User user =
|
||||
userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new UsernameNotFoundException(
|
||||
"No user found with username: " + username));
|
||||
|
||||
if (loginAttemptService.isBlocked(username)) {
|
||||
throw new LockedException("Your account has been locked due to too many failed login attempts.");
|
||||
throw new LockedException(
|
||||
"Your account has been locked due to too many failed login attempts.");
|
||||
}
|
||||
|
||||
return new org.springframework.security.core.userdetails.User(
|
||||
user.getUsername(),
|
||||
user.getPassword(),
|
||||
user.isEnabled(),
|
||||
true, true, true,
|
||||
getAuthorities(user.getAuthorities())
|
||||
);
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
getAuthorities(user.getAuthorities()));
|
||||
}
|
||||
|
||||
private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) {
|
||||
|
@ -20,12 +20,12 @@ import stirling.software.SPDF.utils.RequestUriUtils;
|
||||
@Component
|
||||
public class FirstLoginFilter extends OncePerRequestFilter {
|
||||
|
||||
@Autowired
|
||||
@Lazy
|
||||
private UserService userService;
|
||||
@Autowired @Lazy private UserService userService;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
String method = request.getMethod();
|
||||
String requestURI = request.getRequestURI();
|
||||
// Check if the request is for static resources
|
||||
@ -40,7 +40,10 @@ public class FirstLoginFilter extends OncePerRequestFilter {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
Optional<User> user = userService.findByUsername(authentication.getName());
|
||||
if ("GET".equalsIgnoreCase(method) && user.isPresent() && user.get().isFirstLogin() && !"/change-creds".equals(requestURI)) {
|
||||
if ("GET".equalsIgnoreCase(method)
|
||||
&& user.isPresent()
|
||||
&& user.get().isFirstLogin()
|
||||
&& !"/change-creds".equals(requestURI)) {
|
||||
response.sendRedirect("/change-creds");
|
||||
return;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
package stirling.software.SPDF.config.security;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@ -13,7 +14,8 @@ import stirling.software.SPDF.utils.RequestUriUtils;
|
||||
|
||||
public class IPRateLimitingFilter implements Filter {
|
||||
|
||||
private final ConcurrentHashMap<String, AtomicInteger> requestCounts = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, AtomicInteger> requestCounts =
|
||||
new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, AtomicInteger> getCounts = new ConcurrentHashMap<>();
|
||||
private final int maxRequests;
|
||||
private final int maxGetRequests;
|
||||
@ -24,7 +26,8 @@ public class IPRateLimitingFilter implements Filter {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
if (request instanceof HttpServletRequest) {
|
||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||
String method = httpRequest.getMethod();
|
||||
|
@ -13,39 +13,40 @@ import org.springframework.stereotype.Component;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.Role;
|
||||
|
||||
@Component
|
||||
public class InitialSecuritySetup {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
@Autowired private UserService userService;
|
||||
|
||||
|
||||
@Autowired
|
||||
ApplicationProperties applicationProperties;
|
||||
@Autowired ApplicationProperties applicationProperties;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
if (!userService.hasUsers()) {
|
||||
|
||||
|
||||
String initialUsername = applicationProperties.getSecurity().getInitialLogin().getUsername();
|
||||
String initialPassword = applicationProperties.getSecurity().getInitialLogin().getPassword();
|
||||
String initialUsername =
|
||||
applicationProperties.getSecurity().getInitialLogin().getUsername();
|
||||
String initialPassword =
|
||||
applicationProperties.getSecurity().getInitialLogin().getPassword();
|
||||
if (initialUsername != null && initialPassword != null) {
|
||||
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
|
||||
} else {
|
||||
initialUsername = "admin";
|
||||
initialPassword = "stirling";
|
||||
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId(), true);
|
||||
userService.saveUser(
|
||||
initialUsername, initialPassword, Role.ADMIN.getRoleId(), true);
|
||||
}
|
||||
}
|
||||
if (!userService.usernameExists(Role.INTERNAL_API_USER.getRoleId())) {
|
||||
userService.saveUser(Role.INTERNAL_API_USER.getRoleId(), UUID.randomUUID().toString(), Role.INTERNAL_API_USER.getRoleId());
|
||||
userService.saveUser(
|
||||
Role.INTERNAL_API_USER.getRoleId(),
|
||||
UUID.randomUUID().toString(),
|
||||
Role.INTERNAL_API_USER.getRoleId());
|
||||
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@PostConstruct
|
||||
public void initSecretKey() throws IOException {
|
||||
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
|
||||
|
@ -1,4 +1,5 @@
|
||||
package stirling.software.SPDF.config.security;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@ -12,29 +13,32 @@ import stirling.software.SPDF.model.AttemptCounter;
|
||||
@Service
|
||||
public class LoginAttemptService {
|
||||
|
||||
|
||||
@Autowired
|
||||
ApplicationProperties applicationProperties;
|
||||
@Autowired ApplicationProperties applicationProperties;
|
||||
|
||||
private int MAX_ATTEMPTS;
|
||||
private long ATTEMPT_INCREMENT_TIME;
|
||||
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
MAX_ATTEMPTS = applicationProperties.getSecurity().getLoginAttemptCount();
|
||||
ATTEMPT_INCREMENT_TIME = TimeUnit.MINUTES.toMillis(applicationProperties.getSecurity().getLoginResetTimeMinutes());
|
||||
ATTEMPT_INCREMENT_TIME =
|
||||
TimeUnit.MINUTES.toMillis(
|
||||
applicationProperties.getSecurity().getLoginResetTimeMinutes());
|
||||
}
|
||||
|
||||
private final ConcurrentHashMap<String, AttemptCounter> attemptsCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, AttemptCounter> attemptsCache =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
public void loginSucceeded(String key) {
|
||||
attemptsCache.remove(key);
|
||||
}
|
||||
|
||||
public boolean loginAttemptCheck(String key) {
|
||||
attemptsCache.compute(key, (k, attemptCounter) -> {
|
||||
if (attemptCounter == null || attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) {
|
||||
attemptsCache.compute(
|
||||
key,
|
||||
(k, attemptCounter) -> {
|
||||
if (attemptCounter == null
|
||||
|| attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) {
|
||||
return new AttemptCounter();
|
||||
} else {
|
||||
attemptCounter.increment();
|
||||
@ -44,7 +48,6 @@ public class LoginAttemptService {
|
||||
return attemptsCache.get(key).getAttemptCount() >= MAX_ATTEMPTS;
|
||||
}
|
||||
|
||||
|
||||
public boolean isBlocked(String key) {
|
||||
AttemptCounter attemptCounter = attemptsCache.get(key);
|
||||
if (attemptCounter != null) {
|
||||
@ -52,5 +55,4 @@ public class LoginAttemptService {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
package stirling.software.SPDF.config.security;
|
||||
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
@ -19,36 +19,30 @@ import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
|
||||
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity()
|
||||
@EnableMethodSecurity
|
||||
public class SecurityConfiguration {
|
||||
|
||||
@Autowired
|
||||
private UserDetailsService userDetailsService;
|
||||
@Autowired private UserDetailsService userDetailsService;
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
@Autowired
|
||||
@Lazy
|
||||
private UserService userService;
|
||||
|
||||
@Autowired @Lazy private UserService userService;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("loginEnabled")
|
||||
public boolean loginEnabledValue;
|
||||
|
||||
@Autowired
|
||||
private UserAuthenticationFilter userAuthenticationFilter;
|
||||
@Autowired private UserAuthenticationFilter userAuthenticationFilter;
|
||||
|
||||
@Autowired private LoginAttemptService loginAttemptService;
|
||||
|
||||
|
||||
@Autowired
|
||||
private LoginAttemptService loginAttemptService;
|
||||
|
||||
@Autowired
|
||||
private FirstLoginFilter firstLoginFilter;
|
||||
@Autowired private FirstLoginFilter firstLoginFilter;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
@ -59,64 +53,78 @@ public class SecurityConfiguration {
|
||||
http.csrf(csrf -> csrf.disable());
|
||||
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
http
|
||||
.formLogin(formLogin -> formLogin
|
||||
http.formLogin(
|
||||
formLogin ->
|
||||
formLogin
|
||||
.loginPage("/login")
|
||||
.successHandler(new CustomAuthenticationSuccessHandler())
|
||||
.successHandler(
|
||||
new CustomAuthenticationSuccessHandler())
|
||||
.defaultSuccessUrl("/")
|
||||
.failureHandler(new CustomAuthenticationFailureHandler(loginAttemptService))
|
||||
.permitAll()
|
||||
).requestCache(requestCache -> requestCache
|
||||
.requestCache(new NullRequestCache())
|
||||
)
|
||||
.logout(logout -> logout
|
||||
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
|
||||
.failureHandler(
|
||||
new CustomAuthenticationFailureHandler(
|
||||
loginAttemptService))
|
||||
.permitAll())
|
||||
.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()))
|
||||
.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
|
||||
.deleteCookies("JSESSIONID", "remember-me"))
|
||||
.rememberMe(
|
||||
rememberMeConfigurer ->
|
||||
rememberMeConfigurer // Use the configurator directly
|
||||
.key("uniqueAndSecret")
|
||||
.tokenRepository(persistentTokenRepository())
|
||||
.tokenValiditySeconds(1209600) // 2 weeks
|
||||
)
|
||||
.authorizeHttpRequests(authz -> authz
|
||||
.requestMatchers(req -> {
|
||||
.authorizeHttpRequests(
|
||||
authz ->
|
||||
authz.requestMatchers(
|
||||
req -> {
|
||||
String uri = req.getRequestURI();
|
||||
String contextPath = req.getContextPath();
|
||||
|
||||
// Remove the context path from the URI
|
||||
String trimmedUri = uri.startsWith(contextPath) ? uri.substring(contextPath.length()) : uri;
|
||||
String trimmedUri =
|
||||
uri.startsWith(contextPath)
|
||||
? uri.substring(
|
||||
contextPath
|
||||
.length())
|
||||
: uri;
|
||||
|
||||
return trimmedUri.startsWith("/login") || trimmedUri.endsWith(".svg") ||
|
||||
trimmedUri.startsWith("/register") || trimmedUri.startsWith("/error") ||
|
||||
trimmedUri.startsWith("/images/") || trimmedUri.startsWith("/public/") ||
|
||||
trimmedUri.startsWith("/css/") || trimmedUri.startsWith("/js/");
|
||||
}
|
||||
).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
return trimmedUri.startsWith("/login")
|
||||
|| trimmedUri.endsWith(".svg")
|
||||
|| trimmedUri.startsWith(
|
||||
"/register")
|
||||
|| trimmedUri.startsWith("/error")
|
||||
|| trimmedUri.startsWith("/images/")
|
||||
|| trimmedUri.startsWith("/public/")
|
||||
|| trimmedUri.startsWith("/css/")
|
||||
|| trimmedUri.startsWith("/js/")
|
||||
|| trimmedUri.startsWith(
|
||||
"/api/v1/info/status");
|
||||
})
|
||||
.permitAll()
|
||||
.anyRequest()
|
||||
.authenticated())
|
||||
.userDetailsService(userDetailsService)
|
||||
.authenticationProvider(authenticationProvider());
|
||||
} else {
|
||||
http.csrf(csrf -> csrf.disable())
|
||||
.authorizeHttpRequests(authz -> authz
|
||||
.anyRequest().permitAll()
|
||||
);
|
||||
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||
}
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Bean
|
||||
public IPRateLimitingFilter rateLimitingFilter() {
|
||||
int maxRequestsPerIp = 1000000; // Example limit TODO add config level
|
||||
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Bean
|
||||
public DaoAuthenticationProvider authenticationProvider() {
|
||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
||||
@ -129,8 +137,4 @@ public class SecurityConfiguration {
|
||||
public PersistentTokenRepository persistentTokenRepository() {
|
||||
return new JPATokenRepositoryImpl();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@ -19,25 +19,22 @@ 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 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 {
|
||||
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
|
||||
@ -52,15 +49,17 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
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.
|
||||
// Use API key to authenticate. This requires you to have an authentication
|
||||
// provider for API keys.
|
||||
UserDetails userDetails = userService.loadUserByApiKey(apiKey);
|
||||
if(userDetails == null)
|
||||
{
|
||||
if (userDetails == null) {
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
response.getWriter().write("Invalid API Key.");
|
||||
return;
|
||||
}
|
||||
authentication = new ApiKeyAuthenticationToken(userDetails, apiKey, userDetails.getAuthorities());
|
||||
authentication =
|
||||
new ApiKeyAuthenticationToken(
|
||||
userDetails, apiKey, userDetails.getAuthorities());
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
} catch (AuthenticationException e) {
|
||||
// If API key authentication fails, deny the request
|
||||
@ -81,7 +80,9 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
return;
|
||||
} else {
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
response.getWriter().write("Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected");
|
||||
response.getWriter()
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@ -102,6 +103,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
contextPath + "/css/",
|
||||
contextPath + "/js/",
|
||||
contextPath + "/pdfjs/",
|
||||
contextPath + "/api/v1/info/status",
|
||||
contextPath + "/site.webmanifest"
|
||||
};
|
||||
|
||||
@ -113,5 +115,4 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,28 +20,29 @@ 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 private UserDetailsService userDetailsService;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("rateLimit")
|
||||
public boolean rateLimit;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
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);
|
||||
@ -60,7 +61,8 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
||||
// 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
|
||||
identifier =
|
||||
"API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
|
||||
} else {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
@ -74,14 +76,27 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
||||
identifier = request.getRemoteAddr();
|
||||
}
|
||||
|
||||
Role userRole = getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
|
||||
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);
|
||||
processRequest(
|
||||
userRole.getApiCallsPerDay(),
|
||||
identifier,
|
||||
apiBuckets,
|
||||
request,
|
||||
response,
|
||||
filterChain);
|
||||
} else {
|
||||
// It's a Web UI call
|
||||
processRequest(userRole.getWebCallsPerDay(), identifier, webBuckets, request, response, filterChain);
|
||||
processRequest(
|
||||
userRole.getWebCallsPerDay(),
|
||||
identifier,
|
||||
webBuckets,
|
||||
request,
|
||||
response,
|
||||
filterChain);
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,8 +113,13 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
||||
throw new IllegalStateException("User does not have a valid role.");
|
||||
}
|
||||
|
||||
private void processRequest(int limitPerDay, String identifier, Map<String, Bucket> buckets,
|
||||
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
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);
|
||||
@ -116,10 +136,8 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
||||
}
|
||||
|
||||
private Bucket createUserBucket(int limitPerDay) {
|
||||
Bandwidth limit = Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1)));
|
||||
Bandwidth limit =
|
||||
Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1)));
|
||||
return Bucket.builder().addLimit(limit).build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
package stirling.software.SPDF.config.security;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@ -21,14 +22,13 @@ import stirling.software.SPDF.model.Authority;
|
||||
import stirling.software.SPDF.model.Role;
|
||||
import stirling.software.SPDF.model.User;
|
||||
import stirling.software.SPDF.repository.UserRepository;
|
||||
|
||||
@Service
|
||||
public class UserService implements UserServiceInterface {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
@Autowired private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
@Autowired private PasswordEncoder passwordEncoder;
|
||||
|
||||
public Authentication getAuthentication(String apiKey) {
|
||||
User user = getUserByApiKey(apiKey);
|
||||
@ -49,8 +49,6 @@ public class UserService implements UserServiceInterface{
|
||||
return user.getAuthorities().stream()
|
||||
.map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
|
||||
}
|
||||
|
||||
private String generateApiKey() {
|
||||
@ -62,7 +60,9 @@ public class UserService implements UserServiceInterface{
|
||||
}
|
||||
|
||||
public User addApiKeyToUser(String username) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
User user =
|
||||
userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||
|
||||
user.setApiKey(generateApiKey());
|
||||
@ -74,7 +74,9 @@ public class UserService implements UserServiceInterface{
|
||||
}
|
||||
|
||||
public String getApiKeyForUser(String username) {
|
||||
User user = userRepository.findByUsername(username)
|
||||
User user =
|
||||
userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||
return user.getApiKey();
|
||||
}
|
||||
@ -95,13 +97,11 @@ public class UserService implements UserServiceInterface{
|
||||
return new org.springframework.security.core.userdetails.User(
|
||||
user.getUsername(),
|
||||
user.getPassword(), // you might not need this for API key auth
|
||||
getAuthorities(user)
|
||||
);
|
||||
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);
|
||||
@ -191,7 +191,6 @@ public class UserService implements UserServiceInterface{
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
|
||||
public boolean isPasswordCorrect(User user, String currentPassword) {
|
||||
return passwordEncoder.matches(currentPassword, user.getPassword());
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.CropPdfForm;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@ -31,14 +32,14 @@ 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(@ModelAttribute CropPdfForm form)
|
||||
throws IOException {
|
||||
@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(@ModelAttribute CropPdfForm form) throws IOException {
|
||||
|
||||
|
||||
|
||||
|
||||
PDDocument sourceDocument = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()));
|
||||
PDDocument sourceDocument =
|
||||
PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()));
|
||||
|
||||
PDDocument newDocument = new PDDocument();
|
||||
|
||||
@ -71,7 +72,8 @@ for (int i = 0; i < totalPages; i++) {
|
||||
contentStream.close();
|
||||
|
||||
// Now, set the new page's media box to the cropped size
|
||||
newPage.setMediaBox(new PDRectangle(form.getX(), form.getY(), form.getWidth(), form.getHeight()));
|
||||
newPage.setMediaBox(
|
||||
new PDRectangle(form.getX(), form.getY(), form.getWidth(), form.getHeight()));
|
||||
}
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
@ -80,7 +82,9 @@ newDocument.close();
|
||||
sourceDocument.close();
|
||||
|
||||
byte[] pdfContent = baos.toByteArray();
|
||||
return WebResponseUtils.bytesToWebResponse(pdfContent, form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf");
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
pdfContent,
|
||||
form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||
+ "_cropped.pdf");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,7 +1,15 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.io.MemoryUsageSetting;
|
||||
import org.apache.pdfbox.multipdf.PDFMergerUtility;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
@ -14,19 +22,13 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@ -34,7 +36,6 @@ public class MergeController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MergeController.class);
|
||||
|
||||
|
||||
private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
|
||||
PDDocument mergedDoc = new PDDocument();
|
||||
for (PDDocument doc : documents) {
|
||||
@ -52,8 +53,14 @@ public class MergeController {
|
||||
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);
|
||||
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
|
||||
@ -62,8 +69,14 @@ public class MergeController {
|
||||
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);
|
||||
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
|
||||
@ -87,9 +100,12 @@ public class MergeController {
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
|
||||
@Operation(summary = "Merge multiple PDF files into one",
|
||||
description = "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO")
|
||||
public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest form) throws IOException {
|
||||
@Operation(
|
||||
summary = "Merge multiple PDF files into one",
|
||||
description =
|
||||
"This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO")
|
||||
public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest form)
|
||||
throws IOException {
|
||||
try {
|
||||
MultipartFile[] files = form.getFileInput();
|
||||
Arrays.sort(files, getSortComparator(form.getSortType()));
|
||||
@ -101,11 +117,13 @@ public class MergeController {
|
||||
mergedDoc.addSource(new ByteArrayInputStream(file.getBytes()));
|
||||
}
|
||||
|
||||
mergedDoc.setDestinationFileName(files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf");
|
||||
mergedDoc.setDestinationFileName(
|
||||
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf");
|
||||
mergedDoc.setDestinationStream(docOutputstream);
|
||||
mergedDoc.mergeDocuments(MemoryUsageSetting.setupMainMemoryOnly());
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(docOutputstream.toByteArray(), mergedDoc.getDestinationFileName());
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
docOutputstream.toByteArray(), mergedDoc.getDestinationFileName());
|
||||
} catch (Exception ex) {
|
||||
logger.error("Error in merge pdf process", ex);
|
||||
throw ex;
|
||||
|
@ -1,6 +1,5 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
|
||||
import java.awt.Color;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
@ -23,6 +22,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@ -36,20 +36,25 @@ public class MultiPageLayoutController {
|
||||
@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. Input:PDF Output:PDF Type:SISO"
|
||||
)
|
||||
public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(@ModelAttribute MergeMultiplePagesRequest request)
|
||||
throws IOException {
|
||||
description =
|
||||
"This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(
|
||||
@ModelAttribute MergeMultiplePagesRequest request) throws IOException {
|
||||
|
||||
int pagesPerSheet = request.getPagesPerSheet();
|
||||
MultipartFile file = request.getFileInput();
|
||||
boolean addBorder = request.isAddBorder();
|
||||
|
||||
if (pagesPerSheet != 2 && pagesPerSheet != 3 && pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) {
|
||||
if (pagesPerSheet != 2
|
||||
&& pagesPerSheet != 3
|
||||
&& pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) {
|
||||
throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square");
|
||||
}
|
||||
|
||||
int cols = pagesPerSheet == 2 || pagesPerSheet == 3 ? pagesPerSheet : (int) Math.sqrt(pagesPerSheet);
|
||||
int cols =
|
||||
pagesPerSheet == 2 || pagesPerSheet == 3
|
||||
? pagesPerSheet
|
||||
: (int) Math.sqrt(pagesPerSheet);
|
||||
int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet);
|
||||
|
||||
PDDocument sourceDocument = PDDocument.load(file.getInputStream());
|
||||
@ -61,7 +66,9 @@ public class MultiPageLayoutController {
|
||||
float cellWidth = newPage.getMediaBox().getWidth() / cols;
|
||||
float cellHeight = newPage.getMediaBox().getHeight() / rows;
|
||||
|
||||
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
|
||||
PDPageContentStream contentStream =
|
||||
new PDPageContentStream(
|
||||
newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
|
||||
LayerUtility layerUtility = new LayerUtility(newDocument);
|
||||
|
||||
float borderThickness = 1.5f; // Specify border thickness as required
|
||||
@ -74,7 +81,13 @@ public class MultiPageLayoutController {
|
||||
contentStream.close();
|
||||
newPage = new PDPage(PDRectangle.A4);
|
||||
newDocument.addPage(newPage);
|
||||
contentStream = new PDPageContentStream(newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
|
||||
contentStream =
|
||||
new PDPageContentStream(
|
||||
newDocument,
|
||||
newPage,
|
||||
PDPageContentStream.AppendMode.APPEND,
|
||||
true,
|
||||
true);
|
||||
}
|
||||
|
||||
PDPage sourcePage = sourceDocument.getPage(i);
|
||||
@ -83,12 +96,16 @@ public class MultiPageLayoutController {
|
||||
float scaleHeight = cellHeight / rect.getHeight();
|
||||
float scale = Math.min(scaleWidth, scaleHeight);
|
||||
|
||||
int adjustedPageIndex = i % pagesPerSheet; // This will reset the index for every new page
|
||||
int adjustedPageIndex =
|
||||
i % pagesPerSheet; // This will reset the index for every new page
|
||||
int rowIndex = adjustedPageIndex / cols;
|
||||
int colIndex = adjustedPageIndex % cols;
|
||||
|
||||
float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2;
|
||||
float y = newPage.getMediaBox().getHeight() - ((rowIndex + 1) * cellHeight - (cellHeight - rect.getHeight() * scale) / 2);
|
||||
float y =
|
||||
newPage.getMediaBox().getHeight()
|
||||
- ((rowIndex + 1) * cellHeight
|
||||
- (cellHeight - rect.getHeight() * scale) / 2);
|
||||
|
||||
contentStream.saveGraphicsState();
|
||||
contentStream.transform(Matrix.getTranslateInstance(x, y));
|
||||
@ -108,7 +125,6 @@ public class MultiPageLayoutController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
contentStream.close(); // Close the final content stream
|
||||
sourceDocument.close();
|
||||
|
||||
@ -117,8 +133,8 @@ public class MultiPageLayoutController {
|
||||
newDocument.close();
|
||||
|
||||
byte[] result = baos.toByteArray();
|
||||
return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf");
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
result,
|
||||
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.apache.pdfbox.multipdf.Overlay;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.http.MediaType;
|
||||
@ -18,17 +20,23 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.OverlayPdfsRequest;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class PdfOverlayController {
|
||||
|
||||
@PostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data")
|
||||
@Operation(summary = "Overlay PDF files in various modes", description = "Overlay PDF files onto a base PDF with different modes: Sequential, Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO")
|
||||
public ResponseEntity<byte[]> overlayPdfs(@ModelAttribute OverlayPdfsRequest request) throws IOException {
|
||||
@Operation(
|
||||
summary = "Overlay PDF files in various modes",
|
||||
description =
|
||||
"Overlay PDF files onto a base PDF with different modes: Sequential, Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO")
|
||||
public ResponseEntity<byte[]> overlayPdfs(@ModelAttribute OverlayPdfsRequest request)
|
||||
throws IOException {
|
||||
MultipartFile baseFile = request.getFileInput();
|
||||
int overlayPos = request.getOverlayPosition();
|
||||
|
||||
@ -41,12 +49,19 @@ public class PdfOverlayController {
|
||||
overlayPdfFiles[i] = GeneralUtils.multipartToFile(overlayFiles[i]);
|
||||
}
|
||||
|
||||
String mode = request.getOverlayMode(); // "SequentialOverlay", "InterleavedOverlay", "FixedRepeatOverlay"
|
||||
String mode = request.getOverlayMode(); // "SequentialOverlay", "InterleavedOverlay",
|
||||
// "FixedRepeatOverlay"
|
||||
int[] counts = request.getCounts(); // Used for FixedRepeatOverlay mode
|
||||
|
||||
try (PDDocument basePdf = PDDocument.load(baseFile.getInputStream());
|
||||
Overlay overlay = new Overlay()) {
|
||||
Map<Integer, String> overlayGuide = prepareOverlayGuide(basePdf.getNumberOfPages(), overlayPdfFiles, mode, counts, tempFiles);
|
||||
Map<Integer, String> overlayGuide =
|
||||
prepareOverlayGuide(
|
||||
basePdf.getNumberOfPages(),
|
||||
overlayPdfFiles,
|
||||
mode,
|
||||
counts,
|
||||
tempFiles);
|
||||
|
||||
overlay.setInputPDF(basePdf);
|
||||
if (overlayPos == 0) {
|
||||
@ -58,9 +73,12 @@ public class PdfOverlayController {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
overlay.overlay(overlayGuide).save(outputStream);
|
||||
byte[] data = outputStream.toByteArray();
|
||||
String outputFilename = baseFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf"; // Remove file extension and append .pdf
|
||||
String outputFilename =
|
||||
baseFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||
+ "_overlayed.pdf"; // Remove file extension and append .pdf
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(data, outputFilename, MediaType.APPLICATION_PDF);
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
data, outputFilename, MediaType.APPLICATION_PDF);
|
||||
}
|
||||
} finally {
|
||||
for (File overlayPdfFile : overlayPdfFiles) {
|
||||
@ -76,7 +94,9 @@ public class PdfOverlayController {
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Integer, String> prepareOverlayGuide(int basePageCount, File[] overlayFiles, String mode, int[] counts, List<File> tempFiles) throws IOException {
|
||||
private Map<Integer, String> prepareOverlayGuide(
|
||||
int basePageCount, File[] overlayFiles, String mode, int[] counts, List<File> tempFiles)
|
||||
throws IOException {
|
||||
Map<Integer, String> overlayGuide = new HashMap<>();
|
||||
switch (mode) {
|
||||
case "SequentialOverlay":
|
||||
@ -94,12 +114,19 @@ public class PdfOverlayController {
|
||||
return overlayGuide;
|
||||
}
|
||||
|
||||
private void sequentialOverlay(Map<Integer, String> overlayGuide, File[] overlayFiles, int basePageCount, List<File> tempFiles) throws IOException {
|
||||
private void sequentialOverlay(
|
||||
Map<Integer, String> overlayGuide,
|
||||
File[] overlayFiles,
|
||||
int basePageCount,
|
||||
List<File> tempFiles)
|
||||
throws IOException {
|
||||
int overlayFileIndex = 0;
|
||||
int pageCountInCurrentOverlay = 0;
|
||||
|
||||
for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) {
|
||||
if (pageCountInCurrentOverlay == 0 || pageCountInCurrentOverlay >= getNumberOfPages(overlayFiles[overlayFileIndex])) {
|
||||
if (pageCountInCurrentOverlay == 0
|
||||
|| pageCountInCurrentOverlay
|
||||
>= getNumberOfPages(overlayFiles[overlayFileIndex])) {
|
||||
pageCountInCurrentOverlay = 0;
|
||||
overlayFileIndex = (overlayFileIndex + 1) % overlayFiles.length;
|
||||
}
|
||||
@ -125,13 +152,9 @@ public class PdfOverlayController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private void interleavedOverlay(Map<Integer, String> overlayGuide, File[] overlayFiles, int basePageCount) throws IOException {
|
||||
private void interleavedOverlay(
|
||||
Map<Integer, String> overlayGuide, File[] overlayFiles, int basePageCount)
|
||||
throws IOException {
|
||||
for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) {
|
||||
File overlayFile = overlayFiles[(basePageIndex - 1) % overlayFiles.length];
|
||||
|
||||
@ -145,10 +168,12 @@ public class PdfOverlayController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void fixedRepeatOverlay(Map<Integer, String> overlayGuide, File[] overlayFiles, int[] counts, int basePageCount) throws IOException {
|
||||
private void fixedRepeatOverlay(
|
||||
Map<Integer, String> overlayGuide, File[] overlayFiles, int[] counts, int basePageCount)
|
||||
throws IOException {
|
||||
if (overlayFiles.length != counts.length) {
|
||||
throw new IllegalArgumentException("Counts array length must match the number of overlay files");
|
||||
throw new IllegalArgumentException(
|
||||
"Counts array length must match the number of overlay files");
|
||||
}
|
||||
int currentPage = 1;
|
||||
for (int i = 0; i < overlayFiles.length; i++) {
|
||||
@ -167,7 +192,7 @@ public class PdfOverlayController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Additional classes like OverlayPdfsRequest, WebResponseUtils, etc. are assumed to be defined elsewhere.
|
||||
// Additional classes like OverlayPdfsRequest, WebResponseUtils, etc. are assumed to be defined
|
||||
// elsewhere.
|
||||
|
@ -17,11 +17,13 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.SortTypes;
|
||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@ -30,7 +32,10 @@ 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. Input:PDF Output:PDF Type:SISO")
|
||||
@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(@ModelAttribute PDFWithPageNums request)
|
||||
throws IOException {
|
||||
|
||||
@ -42,22 +47,20 @@ public class RearrangePagesPDFController {
|
||||
// Split the page order string into an array of page numbers or range of numbers
|
||||
String[] pageOrderArr = pagesToDelete.split(",");
|
||||
|
||||
List<Integer> pagesToRemove = GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages());
|
||||
List<Integer> pagesToRemove =
|
||||
GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages());
|
||||
|
||||
for (int i = pagesToRemove.size() - 1; i >= 0; i--) {
|
||||
int pageIndex = pagesToRemove.get(i);
|
||||
document.removePage(pageIndex);
|
||||
}
|
||||
return WebResponseUtils.pdfDocToWebResponse(document,
|
||||
return WebResponseUtils.pdfDocToWebResponse(
|
||||
document,
|
||||
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf");
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private List<Integer> removeFirst(int totalPages) {
|
||||
if (totalPages <= 1)
|
||||
return new ArrayList<>();
|
||||
if (totalPages <= 1) return new ArrayList<>();
|
||||
List<Integer> newPageOrder = new ArrayList<>();
|
||||
for (int i = 2; i <= totalPages; i++) {
|
||||
newPageOrder.add(i - 1);
|
||||
@ -66,8 +69,7 @@ public class RearrangePagesPDFController {
|
||||
}
|
||||
|
||||
private List<Integer> removeLast(int totalPages) {
|
||||
if (totalPages <= 1)
|
||||
return new ArrayList<>();
|
||||
if (totalPages <= 1) return new ArrayList<>();
|
||||
List<Integer> newPageOrder = new ArrayList<>();
|
||||
for (int i = 1; i < totalPages; i++) {
|
||||
newPageOrder.add(i - 1);
|
||||
@ -76,8 +78,7 @@ public class RearrangePagesPDFController {
|
||||
}
|
||||
|
||||
private List<Integer> removeFirstAndLast(int totalPages) {
|
||||
if (totalPages <= 2)
|
||||
return new ArrayList<>();
|
||||
if (totalPages <= 2) return new ArrayList<>();
|
||||
List<Integer> newPageOrder = new ArrayList<>();
|
||||
for (int i = 2; i < totalPages; i++) {
|
||||
newPageOrder.add(i - 1);
|
||||
@ -167,8 +168,12 @@ 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. Input:PDF Output:PDF")
|
||||
public ResponseEntity<byte[]> rearrangePages(@ModelAttribute RearrangePagesRequest request) throws IOException {
|
||||
@Operation(
|
||||
summary = "Rearrange pages in a PDF file",
|
||||
description =
|
||||
"This endpoint rearranges pages in a given PDF file based on the specified page order or custom mode. Users can provide a page order as a comma-separated list of page numbers or page ranges, or a custom mode. Input:PDF Output:PDF")
|
||||
public ResponseEntity<byte[]> rearrangePages(@ModelAttribute RearrangePagesRequest request)
|
||||
throws IOException {
|
||||
MultipartFile pdfFile = request.getFileInput();
|
||||
String pageOrder = request.getPageNumbers();
|
||||
String sortType = request.getCustomMode();
|
||||
@ -203,14 +208,13 @@ public class RearrangePagesPDFController {
|
||||
document.addPage(page);
|
||||
}
|
||||
|
||||
return WebResponseUtils.pdfDocToWebResponse(document,
|
||||
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rearranged.pdf");
|
||||
return WebResponseUtils.pdfDocToWebResponse(
|
||||
document,
|
||||
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||
+ "_rearranged.pdf");
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed rearranging documents", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@ -29,10 +30,10 @@ 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. Input:PDF Output:PDF Type:SISO"
|
||||
)
|
||||
public ResponseEntity<byte[]> rotatePDF(
|
||||
@ModelAttribute RotatePDFRequest request) throws IOException {
|
||||
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(@ModelAttribute RotatePDFRequest request)
|
||||
throws IOException {
|
||||
MultipartFile pdfFile = request.getFileInput();
|
||||
Integer angle = request.getAngle();
|
||||
// Load the PDF document
|
||||
@ -45,8 +46,8 @@ public class RotationController {
|
||||
page.setRotation(page.getRotation() + angle);
|
||||
}
|
||||
|
||||
return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rotated.pdf");
|
||||
|
||||
return WebResponseUtils.pdfDocToWebResponse(
|
||||
document,
|
||||
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rotated.pdf");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -23,8 +23,10 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.ScalePagesRequest;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@ -33,8 +35,12 @@ 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. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> scalePages(@ModelAttribute ScalePagesRequest request) throws IOException {
|
||||
@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(@ModelAttribute ScalePagesRequest request)
|
||||
throws IOException {
|
||||
MultipartFile file = request.getFileInput();
|
||||
String targetPDRectangle = request.getPageSize();
|
||||
float scaleFactor = request.getScaleFactor();
|
||||
@ -75,7 +81,9 @@ public class ScalePagesController {
|
||||
PDPage newPage = new PDPage(targetSize);
|
||||
outputDocument.addPage(newPage);
|
||||
|
||||
PDPageContentStream contentStream = new PDPageContentStream(outputDocument, newPage, PDPageContentStream.AppendMode.APPEND, true);
|
||||
PDPageContentStream contentStream =
|
||||
new PDPageContentStream(
|
||||
outputDocument, newPage, PDPageContentStream.AppendMode.APPEND, true);
|
||||
|
||||
float x = (targetSize.getWidth() - sourceSize.getWidth() * scale) / 2;
|
||||
float y = (targetSize.getHeight() - sourceSize.getHeight() * scale) / 2;
|
||||
@ -92,19 +100,13 @@ public class ScalePagesController {
|
||||
contentStream.close();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
outputDocument.save(baos);
|
||||
outputDocument.close();
|
||||
sourceDocument.close();
|
||||
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(),
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
baos.toByteArray(),
|
||||
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@ -36,9 +37,12 @@ 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. Input:PDF Output:PDF Type:SIMO")
|
||||
public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request) throws IOException {
|
||||
@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. Input:PDF Output:PDF Type:SIMO")
|
||||
public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request)
|
||||
throws IOException {
|
||||
MultipartFile file = request.getFileInput();
|
||||
String pages = request.getPageNumbers();
|
||||
// open the pdf document
|
||||
@ -48,7 +52,9 @@ public class SplitPDFController {
|
||||
List<Integer> pageNumbers = request.getPageNumbersList(document);
|
||||
if (!pageNumbers.contains(document.getNumberOfPages() - 1))
|
||||
pageNumbers.add(document.getNumberOfPages() - 1);
|
||||
logger.info("Splitting PDF into pages: {}", pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
|
||||
logger.info(
|
||||
"Splitting PDF into pages: {}",
|
||||
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
|
||||
|
||||
// split the document
|
||||
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
|
||||
@ -72,7 +78,6 @@ public class SplitPDFController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// closing the original document
|
||||
document.close();
|
||||
|
||||
@ -104,8 +109,7 @@ public class SplitPDFController {
|
||||
Files.delete(zipFile);
|
||||
|
||||
// return the Resource in the response
|
||||
return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@ -25,17 +26,22 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class SplitPdfBySectionsController {
|
||||
|
||||
|
||||
@PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data")
|
||||
@Operation(summary = "Split PDF pages into smaller sections", description = "Split each page of a PDF into smaller sections based on the user's choice (halves, thirds, quarters, etc.), both vertically and horizontally. Input:PDF Output:ZIP-PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfBySectionsRequest request) throws Exception {
|
||||
@Operation(
|
||||
summary = "Split PDF pages into smaller sections",
|
||||
description =
|
||||
"Split each page of a PDF into smaller sections based on the user's choice (halves, thirds, quarters, etc.), both vertically and horizontally. Input:PDF Output:ZIP-PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfBySectionsRequest request)
|
||||
throws Exception {
|
||||
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
|
||||
|
||||
MultipartFile file = request.getFileInput();
|
||||
@ -59,8 +65,6 @@ public class SplitPdfBySectionsController {
|
||||
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
|
||||
byte[] data;
|
||||
|
||||
|
||||
|
||||
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
|
||||
int pageNum = 1;
|
||||
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
|
||||
@ -82,10 +86,13 @@ public class SplitPdfBySectionsController {
|
||||
Files.delete(zipFile);
|
||||
}
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
|
||||
public List<PDDocument> splitPdfPages(PDDocument document, int horizontalDivisions, int verticalDivisions) throws IOException {
|
||||
public List<PDDocument> splitPdfPages(
|
||||
PDDocument document, int horizontalDivisions, int verticalDivisions)
|
||||
throws IOException {
|
||||
List<PDDocument> splitDocuments = new ArrayList<>();
|
||||
|
||||
for (PDPage originalPage : document.getPages()) {
|
||||
@ -103,13 +110,20 @@ public class SplitPdfBySectionsController {
|
||||
PDPage subPage = new PDPage(new PDRectangle(subPageWidth, subPageHeight));
|
||||
subDoc.addPage(subPage);
|
||||
|
||||
PDFormXObject form = layerUtility.importPageAsForm(document, document.getPages().indexOf(originalPage));
|
||||
PDFormXObject form =
|
||||
layerUtility.importPageAsForm(
|
||||
document, document.getPages().indexOf(originalPage));
|
||||
|
||||
try (PDPageContentStream contentStream = new PDPageContentStream(subDoc, subPage)) {
|
||||
try (PDPageContentStream contentStream =
|
||||
new PDPageContentStream(subDoc, subPage)) {
|
||||
// Set clipping area and position
|
||||
float translateX = -subPageWidth * i;
|
||||
float translateY = height - subPageHeight * (verticalDivisions - j);
|
||||
|
||||
|
||||
//Code for google Docs pdfs..
|
||||
//float translateY = -subPageHeight * (verticalDivisions - 1 - j);
|
||||
|
||||
contentStream.saveGraphicsState();
|
||||
contentStream.addRect(0, 0, subPageWidth, subPageHeight);
|
||||
contentStream.clip();
|
||||
@ -127,9 +141,4 @@ public class SplitPdfBySectionsController {
|
||||
|
||||
return splitDocuments;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@ -20,6 +21,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@ -29,15 +31,16 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class SplitPdfBySizeController {
|
||||
|
||||
|
||||
@PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data")
|
||||
@Operation(summary = "Auto split PDF pages into separate documents based on size or count", description = "split PDF into multiple paged documents based on size/count, ie if 20 pages and split into 5, it does 5 documents each 4 pages\r\n"
|
||||
@Operation(
|
||||
summary = "Auto split PDF pages into separate documents based on size or count",
|
||||
description =
|
||||
"split PDF into multiple paged documents based on size/count, ie if 20 pages and split into 5, it does 5 documents each 4 pages\r\n"
|
||||
+ " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB (rounded so that it accepts 1.9MB but not 2.1MB) Input:PDF Output:ZIP-PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request) throws Exception {
|
||||
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request)
|
||||
throws Exception {
|
||||
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<ByteArrayOutputStream>();
|
||||
|
||||
|
||||
|
||||
MultipartFile file = request.getFileInput();
|
||||
PDDocument sourceDocument = PDDocument.load(file.getInputStream());
|
||||
|
||||
@ -115,8 +118,6 @@ public class SplitPdfBySizeController {
|
||||
|
||||
sourceDocument.close();
|
||||
|
||||
|
||||
|
||||
Path zipFile = Files.createTempFile("split_documents", ".zip");
|
||||
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
|
||||
byte[] data;
|
||||
@ -139,7 +140,8 @@ public class SplitPdfBySizeController {
|
||||
Files.delete(zipFile);
|
||||
}
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
|
||||
private ByteArrayOutputStream currentDocToByteArray(PDDocument document) throws IOException {
|
||||
@ -148,6 +150,4 @@ public class SplitPdfBySizeController {
|
||||
document.close();
|
||||
return baos;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -20,8 +20,10 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.PDFFile;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@ -29,13 +31,13 @@ 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(@ModelAttribute PDFFile request) throws IOException {
|
||||
description =
|
||||
"This endpoint converts a multi-page PDF document into a single paged PDF document. The width of the single page will be same as the input's width, but the height will be the sum of all the pages' heights. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> pdfToSinglePage(@ModelAttribute PDFFile request)
|
||||
throws IOException {
|
||||
|
||||
// Load the source document
|
||||
PDDocument sourceDocument = PDDocument.load(request.getFileInput().getInputStream());
|
||||
@ -63,8 +65,12 @@ public class ToSinglePageController {
|
||||
|
||||
// For each page, copy its content to the new page at the correct offset
|
||||
for (PDPage page : sourceDocument.getPages()) {
|
||||
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, sourceDocument.getPages().indexOf(page));
|
||||
AffineTransform af = AffineTransform.getTranslateInstance(0, yOffset - page.getMediaBox().getHeight());
|
||||
PDFormXObject form =
|
||||
layerUtility.importPageAsForm(
|
||||
sourceDocument, sourceDocument.getPages().indexOf(page));
|
||||
AffineTransform af =
|
||||
AffineTransform.getTranslateInstance(
|
||||
0, yOffset - page.getMediaBox().getHeight());
|
||||
layerUtility.wrapInSaveRestore(newPage);
|
||||
String defaultLayerName = "Layer" + sourceDocument.getPages().indexOf(page);
|
||||
layerUtility.appendFormAsLayer(newPage, form, af, defaultLayerName);
|
||||
@ -77,10 +83,9 @@ public class ToSinglePageController {
|
||||
sourceDocument.close();
|
||||
|
||||
byte[] result = baos.toByteArray();
|
||||
return WebResponseUtils.bytesToWebResponse(result, request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_singlePage.pdf");
|
||||
|
||||
|
||||
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
result,
|
||||
request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||
+ "_singlePage.pdf");
|
||||
}
|
||||
}
|
@ -30,12 +30,12 @@ import stirling.software.SPDF.model.User;
|
||||
@RequestMapping("/api/v1/user")
|
||||
public class UserController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
@Autowired private UserService userService;
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@PostMapping("/register")
|
||||
public String register(@RequestParam String username, @RequestParam String password, Model model) {
|
||||
public String register(
|
||||
@RequestParam String username, @RequestParam String password, Model model) {
|
||||
if (userService.usernameExists(username)) {
|
||||
model.addAttribute("error", "Username already exists");
|
||||
return "register";
|
||||
@ -47,7 +47,8 @@ public class UserController {
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@PostMapping("/change-username-and-password")
|
||||
public RedirectView changeUsernameAndPassword(Principal principal,
|
||||
public RedirectView changeUsernameAndPassword(
|
||||
Principal principal,
|
||||
@RequestParam String currentPassword,
|
||||
@RequestParam String newUsername,
|
||||
@RequestParam String newPassword,
|
||||
@ -74,9 +75,10 @@ public class UserController {
|
||||
return new RedirectView("/change-creds?messageType=usernameExists");
|
||||
}
|
||||
|
||||
|
||||
userService.changePassword(user, newPassword);
|
||||
if(newUsername != null && newUsername.length() > 0 && !user.getUsername().equals(newUsername)) {
|
||||
if (newUsername != null
|
||||
&& newUsername.length() > 0
|
||||
&& !user.getUsername().equals(newUsername)) {
|
||||
userService.changeUsername(user, newUsername);
|
||||
}
|
||||
userService.changeFirstUse(user, false);
|
||||
@ -87,10 +89,10 @@ public class UserController {
|
||||
return new RedirectView("/login?messageType=credsUpdated");
|
||||
}
|
||||
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@PostMapping("/change-username")
|
||||
public RedirectView changeUsername(Principal principal,
|
||||
public RedirectView changeUsername(
|
||||
Principal principal,
|
||||
@RequestParam String currentPassword,
|
||||
@RequestParam String newUsername,
|
||||
HttpServletRequest request,
|
||||
@ -128,7 +130,8 @@ public class UserController {
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@PostMapping("/change-password")
|
||||
public RedirectView changePassword(Principal principal,
|
||||
public RedirectView changePassword(
|
||||
Principal principal,
|
||||
@RequestParam String currentPassword,
|
||||
@RequestParam String newPassword,
|
||||
HttpServletRequest request,
|
||||
@ -180,8 +183,12 @@ public class UserController {
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@PostMapping("/admin/saveUser")
|
||||
public RedirectView saveUser(@RequestParam String username, @RequestParam String password, @RequestParam String role,
|
||||
@RequestParam(name = "forceChange", required = false, defaultValue = "false") boolean forceChange) {
|
||||
public RedirectView saveUser(
|
||||
@RequestParam String username,
|
||||
@RequestParam String password,
|
||||
@RequestParam String role,
|
||||
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
|
||||
boolean forceChange) {
|
||||
|
||||
if (userService.usernameExists(username)) {
|
||||
return new RedirectView("/addUsers?messageType=usernameExists");
|
||||
@ -202,7 +209,6 @@ public class UserController {
|
||||
return new RedirectView("/addUsers"); // Redirect to account page after adding the user
|
||||
}
|
||||
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@PostMapping("/admin/deleteUser/{username}")
|
||||
public String deleteUser(@PathVariable String username, Authentication authentication) {
|
||||
@ -247,6 +253,4 @@ public class UserController {
|
||||
}
|
||||
return ResponseEntity.ok(apiKey);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.GeneralFile;
|
||||
import stirling.software.SPDF.utils.FileToPdf;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@ -18,35 +19,30 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@RequestMapping("/api/v1/convert")
|
||||
public class ConvertHtmlToPDF {
|
||||
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
||||
@Operation(
|
||||
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
|
||||
description = "This endpoint takes an HTML or ZIP file input and converts it to a PDF format."
|
||||
)
|
||||
public ResponseEntity<byte[]> HtmlToPdf(
|
||||
@ModelAttribute GeneralFile request)
|
||||
throws Exception {
|
||||
description =
|
||||
"This endpoint takes an HTML or ZIP file input and converts it to a PDF format.")
|
||||
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute GeneralFile request) throws Exception {
|
||||
MultipartFile fileInput = request.getFileInput();
|
||||
|
||||
if (fileInput == null) {
|
||||
throw new IllegalArgumentException("Please provide an HTML or ZIP file for conversion.");
|
||||
throw new IllegalArgumentException(
|
||||
"Please provide an HTML or ZIP file for conversion.");
|
||||
}
|
||||
|
||||
String originalFilename = fileInput.getOriginalFilename();
|
||||
if (originalFilename == null || (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) {
|
||||
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);
|
||||
}
|
||||
byte[] pdfBytes = FileToPdf.convertHtmlToPdf(fileInput.getBytes(), originalFilename);
|
||||
|
||||
String outputFilename = originalFilename.replaceFirst("[.][^.]+$", "") + ".pdf"; // Remove file extension and append .pdf
|
||||
String outputFilename =
|
||||
originalFilename.replaceFirst("[.][^.]+$", "")
|
||||
+ ".pdf"; // Remove file extension and append .pdf
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
||||
import stirling.software.SPDF.utils.PdfUtils;
|
||||
@ -33,9 +34,12 @@ public class ConvertImgPDFController {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class);
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/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. Input:PDF Output:Image Type:SI-Conditional")
|
||||
public ResponseEntity<Resource> convertToImage(@ModelAttribute ConvertToImageRequest request) throws IOException {
|
||||
@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. Input:PDF Output:Image Type:SI-Conditional")
|
||||
public ResponseEntity<Resource> convertToImage(@ModelAttribute ConvertToImageRequest request)
|
||||
throws IOException {
|
||||
MultipartFile file = request.getFileInput();
|
||||
String imageFormat = request.getImageFormat();
|
||||
String singleOrMultiple = request.getSingleOrMultiple();
|
||||
@ -54,7 +58,14 @@ public class ConvertImgPDFController {
|
||||
byte[] result = null;
|
||||
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
|
||||
try {
|
||||
result = PdfUtils.convertFromPdf(pdfBytes, imageFormat.toUpperCase(), colorTypeResult, singleImage, Integer.valueOf(dpi), filename);
|
||||
result =
|
||||
PdfUtils.convertFromPdf(
|
||||
pdfBytes,
|
||||
imageFormat.toUpperCase(),
|
||||
colorTypeResult,
|
||||
singleImage,
|
||||
Integer.valueOf(dpi),
|
||||
filename);
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
@ -65,21 +76,29 @@ public class ConvertImgPDFController {
|
||||
if (singleImage) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.parseMediaType(getMediaType(imageFormat)));
|
||||
ResponseEntity<Resource> response = new ResponseEntity<>(new ByteArrayResource(result), headers, HttpStatus.OK);
|
||||
ResponseEntity<Resource> response =
|
||||
new ResponseEntity<>(new ByteArrayResource(result), headers, HttpStatus.OK);
|
||||
return response;
|
||||
} else {
|
||||
ByteArrayResource resource = new ByteArrayResource(result);
|
||||
// return the Resource in the response
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename + "_convertedToImages.zip")
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(resource.contentLength()).body(resource);
|
||||
.header(
|
||||
HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename=" + filename + "_convertedToImages.zip")
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.contentLength(resource.contentLength())
|
||||
.body(resource);
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/img/pdf")
|
||||
@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. Input:Image Output:PDF Type:SISO?")
|
||||
public ResponseEntity<byte[]> convertToPdf(@ModelAttribute ConvertToPdfRequest request) throws IOException {
|
||||
@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. Input:Image Output:PDF Type:SISO?")
|
||||
public ResponseEntity<byte[]> convertToPdf(@ModelAttribute ConvertToPdfRequest request)
|
||||
throws IOException {
|
||||
MultipartFile[] file = request.getFileInput();
|
||||
String fitOption = request.getFitOption();
|
||||
String colorType = request.getColorType();
|
||||
@ -87,7 +106,9 @@ public class ConvertImgPDFController {
|
||||
|
||||
// Convert the file to PDF and get the resulting bytes
|
||||
byte[] bytes = PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType);
|
||||
return WebResponseUtils.bytesToWebResponse(bytes, file[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_converted.pdf");
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
bytes,
|
||||
file[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_converted.pdf");
|
||||
}
|
||||
|
||||
private String getMediaType(String imageFormat) {
|
||||
|
@ -12,6 +12,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.GeneralFile;
|
||||
import stirling.software.SPDF.utils.FileToPdf;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@ -24,10 +25,9 @@ public class ConvertMarkdownToPdf {
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/markdown/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(
|
||||
@ModelAttribute GeneralFile request)
|
||||
description =
|
||||
"This endpoint takes a Markdown file input, converts it to HTML, and then to PDF format.")
|
||||
public ResponseEntity<byte[]> markdownToPdf(@ModelAttribute GeneralFile request)
|
||||
throws Exception {
|
||||
MultipartFile fileInput = request.getFileInput();
|
||||
|
||||
@ -48,7 +48,9 @@ public class ConvertMarkdownToPdf {
|
||||
|
||||
byte[] pdfBytes = FileToPdf.convertHtmlToPdf(htmlContent.getBytes(), "converted.html");
|
||||
|
||||
String outputFilename = originalFilename.replaceFirst("[.][^.]+$", "") + ".pdf"; // Remove file extension and append .pdf
|
||||
String outputFilename =
|
||||
originalFilename.replaceFirst("[.][^.]+$", "")
|
||||
+ ".pdf"; // Remove file extension and append .pdf
|
||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.GeneralFile;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||
@ -31,20 +32,33 @@ public class ConvertOfficeController {
|
||||
public byte[] convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
|
||||
// Check for valid file extension
|
||||
String originalFilename = inputFile.getOriginalFilename();
|
||||
if (originalFilename == null || !isValidFileExtension(FilenameUtils.getExtension(originalFilename))) {
|
||||
if (originalFilename == null
|
||||
|| !isValidFileExtension(FilenameUtils.getExtension(originalFilename))) {
|
||||
throw new IllegalArgumentException("Invalid file extension");
|
||||
}
|
||||
|
||||
// Save the uploaded file to a temporary location
|
||||
Path tempInputFile = Files.createTempFile("input_", "." + FilenameUtils.getExtension(originalFilename));
|
||||
Path tempInputFile =
|
||||
Files.createTempFile("input_", "." + FilenameUtils.getExtension(originalFilename));
|
||||
Files.copy(inputFile.getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
// Prepare the output file path
|
||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||
|
||||
// Run the LibreOffice command
|
||||
List<String> command = new ArrayList<>(Arrays.asList("unoconv", "-vvv", "-f", "pdf", "-o", tempOutputFile.toString(), tempInputFile.toString()));
|
||||
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE).runCommandWithOutputHandling(command);
|
||||
List<String> command =
|
||||
new ArrayList<>(
|
||||
Arrays.asList(
|
||||
"unoconv",
|
||||
"-vvv",
|
||||
"-f",
|
||||
"pdf",
|
||||
"-o",
|
||||
tempOutputFile.toString(),
|
||||
tempInputFile.toString()));
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
// Read the converted PDF file
|
||||
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||
@ -55,6 +69,7 @@ public class ConvertOfficeController {
|
||||
|
||||
return pdfBytes;
|
||||
}
|
||||
|
||||
private boolean isValidFileExtension(String fileExtension) {
|
||||
String extensionPattern = "^(?i)[a-z0-9]{2,4}$";
|
||||
return fileExtension.matches(extensionPattern);
|
||||
@ -63,8 +78,8 @@ public class ConvertOfficeController {
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/file/pdf")
|
||||
@Operation(
|
||||
summary = "Convert a file to a PDF using LibreOffice",
|
||||
description = "This endpoint converts a given file to a PDF using LibreOffice API Input:Any Output:PDF Type:SISO"
|
||||
)
|
||||
description =
|
||||
"This endpoint converts a given file to a PDF using LibreOffice API Input:ANY Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> processFileToPDF(@ModelAttribute GeneralFile request)
|
||||
throws Exception {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
@ -72,7 +87,9 @@ public class ConvertOfficeController {
|
||||
// LibreOfficeListener.getInstance().start();
|
||||
|
||||
byte[] pdfByteArray = convertToPdf(inputFile);
|
||||
return WebResponseUtils.bytesToWebResponse(pdfByteArray, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_convertedToPDF.pdf");
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
pdfByteArray,
|
||||
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||
+ "_convertedToPDF.pdf");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.PDFFile;
|
||||
import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest;
|
||||
import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest;
|
||||
@ -23,7 +24,10 @@ import stirling.software.SPDF.utils.PDFToFile;
|
||||
public class ConvertPDFToOffice {
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/html")
|
||||
@Operation(summary = "Convert PDF to HTML", description = "This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO")
|
||||
@Operation(
|
||||
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(@ModelAttribute PDFFile request)
|
||||
throws Exception {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
@ -32,8 +36,13 @@ public class ConvertPDFToOffice {
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/presentation")
|
||||
@Operation(summary = "Convert PDF to Presentation format", description = "This endpoint converts a given PDF file to a Presentation format. Input:PDF Output:PPT Type:SISO")
|
||||
public ResponseEntity<byte[]> processPdfToPresentation(@ModelAttribute PdfToPresentationRequest request) throws IOException, InterruptedException {
|
||||
@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(
|
||||
@ModelAttribute PdfToPresentationRequest request)
|
||||
throws IOException, InterruptedException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
String outputFormat = request.getOutputFormat();
|
||||
PDFToFile pdfToFile = new PDFToFile();
|
||||
@ -41,8 +50,13 @@ public class ConvertPDFToOffice {
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/text")
|
||||
@Operation(summary = "Convert PDF to Text or RTF format", description = "This endpoint converts a given PDF file to Text or RTF format. Input:PDF Output:TXT Type:SISO")
|
||||
public ResponseEntity<byte[]> processPdfToRTForTXT(@ModelAttribute PdfToTextOrRTFRequest request) throws IOException, InterruptedException {
|
||||
@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(
|
||||
@ModelAttribute PdfToTextOrRTFRequest request)
|
||||
throws IOException, InterruptedException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
String outputFormat = request.getOutputFormat();
|
||||
|
||||
@ -51,8 +65,12 @@ public class ConvertPDFToOffice {
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/word")
|
||||
@Operation(summary = "Convert PDF to Word document", description = "This endpoint converts a given PDF file to a Word document format. Input:PDF Output:WORD Type:SISO")
|
||||
public ResponseEntity<byte[]> processPdfToWord(@ModelAttribute PdfToWordRequest request) throws IOException, InterruptedException {
|
||||
@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(@ModelAttribute PdfToWordRequest request)
|
||||
throws IOException, InterruptedException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
String outputFormat = request.getOutputFormat();
|
||||
PDFToFile pdfToFile = new PDFToFile();
|
||||
@ -60,7 +78,10 @@ public class ConvertPDFToOffice {
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/xml")
|
||||
@Operation(summary = "Convert PDF to XML", description = "This endpoint converts a PDF file to an XML file. Input:PDF Output:XML Type:SISO")
|
||||
@Operation(
|
||||
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(@ModelAttribute PDFFile request)
|
||||
throws Exception {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
@ -68,5 +89,4 @@ public class ConvertPDFToOffice {
|
||||
PDFToFile pdfToFile = new PDFToFile();
|
||||
return pdfToFile.processPdfToOfficeFormat(inputFile, "xml", "writer_pdf_import");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.PDFFile;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||
@ -27,10 +28,9 @@ public class ConvertPDFToPDFA {
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/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. Input:PDF Output:PDF Type:SISO"
|
||||
)
|
||||
public ResponseEntity<byte[]> pdfToPdfA(@ModelAttribute PDFFile request)
|
||||
throws Exception {
|
||||
description =
|
||||
"This endpoint converts a PDF file to a PDF/A file. PDF/A is a format designed for long-term archiving of digital documents. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> pdfToPdfA(@ModelAttribute PDFFile request) throws Exception {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
|
||||
// Save the uploaded file to a temporary location
|
||||
@ -50,7 +50,9 @@ public class ConvertPDFToPDFA {
|
||||
command.add(tempInputFile.toString());
|
||||
command.add(tempOutputFile.toString());
|
||||
|
||||
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command);
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
// Read the optimized PDF file
|
||||
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||
@ -60,8 +62,8 @@ public class ConvertPDFToPDFA {
|
||||
Files.delete(tempOutputFile);
|
||||
|
||||
// Return the optimized PDF as a response
|
||||
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_PDFA.pdf";
|
||||
String outputFilename =
|
||||
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_PDFA.pdf";
|
||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
@ -28,9 +29,10 @@ public class ConvertWebsiteToPDF {
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
|
||||
@Operation(
|
||||
summary = "Convert a URL to a PDF",
|
||||
description = "This endpoint fetches content from a URL and converts it to a PDF format. Input:N/A Output:PDF Type:SISO"
|
||||
)
|
||||
public ResponseEntity<byte[]> urlToPdf(@ModelAttribute UrlToPdfRequest request) throws IOException, InterruptedException {
|
||||
description =
|
||||
"This endpoint fetches content from a URL and converts it to a PDF format. Input:N/A Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> urlToPdf(@ModelAttribute UrlToPdfRequest request)
|
||||
throws IOException, InterruptedException {
|
||||
String URL = request.getUrlInput();
|
||||
|
||||
// Validate the URL format
|
||||
@ -49,12 +51,13 @@ public class ConvertWebsiteToPDF {
|
||||
command.add(URL);
|
||||
command.add(tempOutputFile.toString());
|
||||
|
||||
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT).runCommandWithOutputHandling(command);
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
// Read the optimized PDF file
|
||||
pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||
}
|
||||
finally {
|
||||
} finally {
|
||||
// Clean up the temporary files
|
||||
Files.delete(tempOutputFile);
|
||||
}
|
||||
@ -71,6 +74,4 @@ public class ConvertWebsiteToPDF {
|
||||
}
|
||||
return safeName + ".pdf";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import com.opencsv.CSVWriter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.controller.api.CropController;
|
||||
import stirling.software.SPDF.controller.api.strippers.PDFTableStripper;
|
||||
import stirling.software.SPDF.model.api.extract.PDFFilePage;
|
||||
@ -34,14 +35,17 @@ public class ExtractController {
|
||||
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
|
||||
|
||||
@PostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
|
||||
@Operation(summary = "Extracts a PDF document to csv", description = "This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO")
|
||||
public ResponseEntity<String> PdfToCsv(@ModelAttribute PDFFilePage form)
|
||||
throws Exception {
|
||||
@Operation(
|
||||
summary = "Extracts a PDF document to csv",
|
||||
description =
|
||||
"This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO")
|
||||
public ResponseEntity<String> PdfToCsv(@ModelAttribute PDFFilePage form) throws Exception {
|
||||
|
||||
ArrayList<String> tableData = new ArrayList<>();
|
||||
int columnsCount = 0;
|
||||
|
||||
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) {
|
||||
try (PDDocument document =
|
||||
PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) {
|
||||
final double res = 72; // PDF units are at 72 DPI
|
||||
PDFTableStripper stripper = new PDFTableStripper();
|
||||
PDPage pdPage = document.getPage(form.getPageId() - 1);
|
||||
@ -64,8 +68,15 @@ public class ExtractController {
|
||||
}
|
||||
}
|
||||
|
||||
List<String> fullTable = notEmptyColumns.stream().map((entity)->
|
||||
entity.replace('\n',' ').replace('\r',' ').trim().replaceAll("\\s{2,}", "|")).toList();
|
||||
List<String> fullTable =
|
||||
notEmptyColumns.stream()
|
||||
.map(
|
||||
(entity) ->
|
||||
entity.replace('\n', ' ')
|
||||
.replace('\r', ' ')
|
||||
.trim()
|
||||
.replaceAll("\\s{2,}", "|"))
|
||||
.toList();
|
||||
|
||||
int rowsCount = fullTable.get(0).split("\\|").length;
|
||||
|
||||
@ -85,12 +96,17 @@ public class ExtractController {
|
||||
}
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentDisposition(ContentDisposition.builder("attachment").filename(form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_extracted.csv").build());
|
||||
headers.setContentDisposition(
|
||||
ContentDisposition.builder("attachment")
|
||||
.filename(
|
||||
form.getFileInput()
|
||||
.getOriginalFilename()
|
||||
.replaceFirst("[.][^.]+$", "")
|
||||
+ "_extracted.csv")
|
||||
.build());
|
||||
headers.setContentType(MediaType.parseMediaType("text/csv"));
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.headers(headers)
|
||||
.body(writer.toString());
|
||||
return ResponseEntity.ok().headers(headers).body(writer.toString());
|
||||
}
|
||||
|
||||
private ArrayList<String> getRecordsList(int rowsCounts, List<String> items) {
|
||||
@ -111,6 +127,7 @@ public class ExtractController {
|
||||
|
||||
return recordsList;
|
||||
}
|
||||
|
||||
private ArrayList<String> getTableHeaders(int columnsCount, List<String> items) {
|
||||
ArrayList<String> resultList = new ArrayList<>();
|
||||
for (int i = 0; i < columnsCount; i++) {
|
||||
|
@ -14,6 +14,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.PDFComparisonAndCount;
|
||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
import stirling.software.SPDF.model.api.filter.ContainsTextRequest;
|
||||
@ -29,21 +30,27 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
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(@ModelAttribute ContainsTextRequest request) throws IOException, InterruptedException {
|
||||
@Operation(
|
||||
summary = "Checks if a PDF contains set text, returns true if does",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
public ResponseEntity<byte[]> containsText(@ModelAttribute ContainsTextRequest request)
|
||||
throws IOException, InterruptedException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
String text = request.getText();
|
||||
String pageNumber = request.getPageNumbers();
|
||||
|
||||
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
|
||||
if (PdfUtils.hasText(pdfDocument, pageNumber, text))
|
||||
return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename());
|
||||
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")
|
||||
@Operation(
|
||||
summary = "Checks if a PDF contains an image",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
public ResponseEntity<byte[]> containsImage(@ModelAttribute PDFWithPageNums request)
|
||||
throws IOException, InterruptedException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
@ -51,13 +58,17 @@ public class FilterController {
|
||||
|
||||
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
|
||||
if (PdfUtils.hasImages(pdfDocument, pageNumber))
|
||||
return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename());
|
||||
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(@ModelAttribute PDFComparisonAndCount request) throws IOException, InterruptedException {
|
||||
@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(@ModelAttribute PDFComparisonAndCount request)
|
||||
throws IOException, InterruptedException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
String pageCount = request.getPageCount();
|
||||
String comparator = request.getComparator();
|
||||
@ -81,14 +92,16 @@ public class FilterController {
|
||||
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
||||
}
|
||||
|
||||
if (valid)
|
||||
return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||
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(@ModelAttribute PageSizeRequest request) throws IOException, InterruptedException {
|
||||
@Operation(
|
||||
summary = "Checks if a PDF is of a certain size",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
public ResponseEntity<byte[]> pageSize(@ModelAttribute PageSizeRequest request)
|
||||
throws IOException, InterruptedException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
String standardPageSize = request.getStandardPageSize();
|
||||
String comparator = request.getComparator();
|
||||
@ -122,14 +135,16 @@ public class FilterController {
|
||||
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
||||
}
|
||||
|
||||
if (valid)
|
||||
return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||
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(@ModelAttribute FileSizeRequest request) throws IOException, InterruptedException {
|
||||
@Operation(
|
||||
summary = "Checks if a PDF is a set file size",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
public ResponseEntity<byte[]> fileSize(@ModelAttribute FileSizeRequest request)
|
||||
throws IOException, InterruptedException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
String fileSize = request.getFileSize();
|
||||
String comparator = request.getComparator();
|
||||
@ -153,14 +168,16 @@ public class FilterController {
|
||||
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
||||
}
|
||||
|
||||
if (valid)
|
||||
return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||
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(@ModelAttribute PageRotationRequest request) throws IOException, InterruptedException {
|
||||
@Operation(
|
||||
summary = "Checks if a PDF is of a certain rotation",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
public ResponseEntity<byte[]> pageRotation(@ModelAttribute PageRotationRequest request)
|
||||
throws IOException, InterruptedException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
int rotation = request.getRotation();
|
||||
String comparator = request.getComparator();
|
||||
@ -187,10 +204,7 @@ public class FilterController {
|
||||
throw new IllegalArgumentException("Invalid comparator: " + comparator);
|
||||
}
|
||||
|
||||
if (valid)
|
||||
return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,8 +19,10 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
@ -32,13 +34,18 @@ public class AutoRenameController {
|
||||
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(@ModelAttribute ExtractHeaderRequest request) throws Exception {
|
||||
@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(@ModelAttribute ExtractHeaderRequest request)
|
||||
throws Exception {
|
||||
MultipartFile file = request.getFileInput();
|
||||
Boolean useFirstTextAsFallback = request.isUseFirstTextAsFallback();
|
||||
|
||||
PDDocument document = PDDocument.load(file.getInputStream());
|
||||
PDFTextStripper reader = new PDFTextStripper() {
|
||||
PDFTextStripper reader =
|
||||
new PDFTextStripper() {
|
||||
class LineInfo {
|
||||
String text;
|
||||
float fontSize;
|
||||
@ -92,7 +99,8 @@ public class AutoRenameController {
|
||||
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) {
|
||||
while (i + 1 < lineInfos.size()
|
||||
&& lineInfos.get(i + 1).fontSize == fontSize) {
|
||||
mergedText += " " + lineInfos.get(i + 1).text;
|
||||
i++;
|
||||
}
|
||||
@ -100,18 +108,24 @@ public class AutoRenameController {
|
||||
}
|
||||
|
||||
// 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;
|
||||
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);
|
||||
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("[/\\\\?%*:|\"<>]", "");
|
||||
@ -121,8 +135,4 @@ public class AutoRenameController {
|
||||
return WebResponseUtils.pdfDocToWebResponse(document, file.getOriginalFilename());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
package stirling.software.SPDF.controller.api.misc;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferByte;
|
||||
import java.awt.image.DataBufferInt;
|
||||
@ -32,6 +33,7 @@ import com.google.zxing.common.HybridBinarizer;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@ -40,11 +42,15 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
public class AutoSplitPdfController {
|
||||
|
||||
private static final String QR_CONTENT = "https://github.com/Frooodle/Stirling-PDF";
|
||||
private static final String QR_CONTENT = "https://github.com/Stirling-Tools/Stirling-PDF";
|
||||
|
||||
@PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data")
|
||||
@Operation(summary = "Auto split PDF pages into separate documents", description = "This endpoint accepts a PDF file, scans each page for a specific QR code, and splits the document at the QR code boundaries. The output is a zip file containing each separate PDF document. Input:PDF Output:ZIP-PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request) throws IOException {
|
||||
@Operation(
|
||||
summary = "Auto split PDF pages into separate documents",
|
||||
description =
|
||||
"This endpoint accepts a PDF file, scans each page for a specific QR code, and splits the document at the QR code boundaries. The output is a zip file containing each separate PDF document. Input:PDF Output:ZIP-PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request)
|
||||
throws IOException {
|
||||
MultipartFile file = request.getFileInput();
|
||||
boolean duplexMode = request.isDuplexMode();
|
||||
|
||||
@ -111,25 +117,44 @@ public class AutoSplitPdfController {
|
||||
Files.delete(zipFile);
|
||||
}
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
|
||||
|
||||
private static String decodeQRCode(BufferedImage bufferedImage) {
|
||||
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);
|
||||
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);
|
||||
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");
|
||||
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));
|
||||
|
@ -28,6 +28,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest;
|
||||
import stirling.software.SPDF.utils.PdfUtils;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
@ -42,9 +43,10 @@ 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. Input:PDF Output:PDF Type:SISO"
|
||||
)
|
||||
public ResponseEntity<byte[]> removeBlankPages(@ModelAttribute RemoveBlankPagesRequest request) throws IOException, InterruptedException {
|
||||
description =
|
||||
"This endpoint removes blank pages from a given PDF file. Users can specify the threshold and white percentage to tune the detection of blank pages. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> removeBlankPages(@ModelAttribute RemoveBlankPagesRequest request)
|
||||
throws IOException, InterruptedException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
int threshold = request.getThreshold();
|
||||
float whitePercent = request.getWhitePercent();
|
||||
@ -79,14 +81,27 @@ public class BlankPageController {
|
||||
BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 300);
|
||||
ImageIO.write(image, "png", tempFile.toFile());
|
||||
|
||||
List<String> command = new ArrayList<>(Arrays.asList("python3", System.getProperty("user.dir") + "/scripts/detect-blank-pages.py", tempFile.toString() ,"--threshold", String.valueOf(threshold), "--white_percent", String.valueOf(whitePercent)));
|
||||
List<String> command =
|
||||
new ArrayList<>(
|
||||
Arrays.asList(
|
||||
"python3",
|
||||
System.getProperty("user.dir")
|
||||
+ "/scripts/detect-blank-pages.py",
|
||||
tempFile.toString(),
|
||||
"--threshold",
|
||||
String.valueOf(threshold),
|
||||
"--white_percent",
|
||||
String.valueOf(whitePercent)));
|
||||
|
||||
// Run CLI command
|
||||
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV).runCommandWithOutputHandling(command);
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
// does contain data
|
||||
if (returnCode.getRc() == 0) {
|
||||
System.out.println("page " + pageIndex + " has image which is not blank");
|
||||
System.out.println(
|
||||
"page " + pageIndex + " has image which is not blank");
|
||||
pagesToKeepIndex.add(pageIndex);
|
||||
} else {
|
||||
System.out.println("Skipping, Image was blank for page #" + pageIndex);
|
||||
@ -94,12 +109,12 @@ public class BlankPageController {
|
||||
}
|
||||
}
|
||||
pageIndex++;
|
||||
|
||||
}
|
||||
System.out.print("pagesToKeep=" + pagesToKeepIndex.size());
|
||||
|
||||
// Remove pages not present in pagesToKeepIndex
|
||||
List<Integer> pageIndices = IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList());
|
||||
List<Integer> pageIndices =
|
||||
IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList());
|
||||
Collections.reverse(pageIndices); // Reverse to prevent index shifting during removal
|
||||
for (Integer i : pageIndices) {
|
||||
if (!pagesToKeepIndex.contains(i)) {
|
||||
@ -107,16 +122,15 @@ public class BlankPageController {
|
||||
}
|
||||
}
|
||||
|
||||
return WebResponseUtils.pdfDocToWebResponse(document, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_blanksRemoved.pdf");
|
||||
return WebResponseUtils.pdfDocToWebResponse(
|
||||
document,
|
||||
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||
+ "_blanksRemoved.pdf");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
} finally {
|
||||
if (document != null)
|
||||
document.close();
|
||||
if (document != null) document.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
@ -44,13 +45,16 @@ 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. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> optimizePdf(@ModelAttribute OptimizePdfRequest request) throws Exception {
|
||||
@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(@ModelAttribute OptimizePdfRequest request)
|
||||
throws Exception {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
Integer optimizeLevel = request.getOptimizeLevel();
|
||||
String expectedOutputSizeString = request.getExpectedOutputSize();
|
||||
|
||||
|
||||
if (expectedOutputSizeString == null && optimizeLevel == null) {
|
||||
throw new Exception("Both expected output size and optimize level are not specified");
|
||||
}
|
||||
@ -71,7 +75,8 @@ public class CompressController {
|
||||
// Prepare the output file path
|
||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||
|
||||
// Determine initial optimization level based on expected size reduction, only if in autoMode
|
||||
// Determine initial optimization level based on expected size reduction, only if in
|
||||
// autoMode
|
||||
if (autoMode) {
|
||||
double sizeReductionRatio = expectedOutputSize / (double) inputFileSize;
|
||||
if (sizeReductionRatio > 0.7) {
|
||||
@ -116,7 +121,9 @@ public class CompressController {
|
||||
command.add("-sOutputFile=" + tempOutputFile.toString());
|
||||
command.add(tempInputFile.toString());
|
||||
|
||||
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(command);
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
// Check if file size is within expected size or not auto mode so instantly finish
|
||||
long outputFileSize = Files.size(tempOutputFile);
|
||||
@ -131,13 +138,12 @@ public class CompressController {
|
||||
} else if (optimizeLevel == 5) {
|
||||
|
||||
} else {
|
||||
System.out.println("Increasing ghostscript optimisation level to " + optimizeLevel);
|
||||
System.out.println(
|
||||
"Increasing ghostscript optimisation level to " + optimizeLevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (expectedOutputSize != null && autoMode) {
|
||||
long outputFileSize = Files.size(tempOutputFile);
|
||||
if (outputFileSize > expectedOutputSize) {
|
||||
@ -166,23 +172,39 @@ public class CompressController {
|
||||
}
|
||||
|
||||
// Otherwise, proceed with the scaling
|
||||
Image scaledImage = bufferedImage.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH);
|
||||
Image scaledImage =
|
||||
bufferedImage.getScaledInstance(
|
||||
newWidth, newHeight, Image.SCALE_SMOOTH);
|
||||
|
||||
// Convert the scaled image back to a BufferedImage
|
||||
BufferedImage scaledBufferedImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
|
||||
scaledBufferedImage.getGraphics().drawImage(scaledImage, 0, 0, null);
|
||||
BufferedImage scaledBufferedImage =
|
||||
new BufferedImage(
|
||||
newWidth,
|
||||
newHeight,
|
||||
BufferedImage.TYPE_INT_RGB);
|
||||
scaledBufferedImage
|
||||
.getGraphics()
|
||||
.drawImage(scaledImage, 0, 0, null);
|
||||
|
||||
// Compress the scaled image
|
||||
ByteArrayOutputStream compressedImageStream = new ByteArrayOutputStream();
|
||||
ImageIO.write(scaledBufferedImage, "jpeg", compressedImageStream);
|
||||
ByteArrayOutputStream compressedImageStream =
|
||||
new ByteArrayOutputStream();
|
||||
ImageIO.write(
|
||||
scaledBufferedImage, "jpeg", compressedImageStream);
|
||||
byte[] imageBytes = compressedImageStream.toByteArray();
|
||||
compressedImageStream.close();
|
||||
|
||||
// Convert compressed image back to PDImageXObject
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(imageBytes);
|
||||
PDImageXObject compressedImage = PDImageXObject.createFromByteArray(doc, imageBytes, image.getCOSObject().toString());
|
||||
ByteArrayInputStream bais =
|
||||
new ByteArrayInputStream(imageBytes);
|
||||
PDImageXObject compressedImage =
|
||||
PDImageXObject.createFromByteArray(
|
||||
doc,
|
||||
imageBytes,
|
||||
image.getCOSObject().toString());
|
||||
|
||||
// Replace the image in the resources with the compressed version
|
||||
// Replace the image in the resources with the compressed
|
||||
// version
|
||||
res.put(name, compressedImage);
|
||||
}
|
||||
}
|
||||
@ -196,14 +218,21 @@ public class CompressController {
|
||||
if (currentSize > expectedOutputSize) {
|
||||
// Log the current file size and scaleFactor
|
||||
|
||||
System.out.println("Current file size: " + FileUtils.byteCountToDisplaySize(currentSize));
|
||||
System.out.println(
|
||||
"Current file size: "
|
||||
+ FileUtils.byteCountToDisplaySize(currentSize));
|
||||
System.out.println("Current scale factor: " + scaleFactor);
|
||||
|
||||
// The file is still too large, reduce scaleFactor and try again
|
||||
scaleFactor *= 0.9; // reduce scaleFactor by 10%
|
||||
// Avoid scaleFactor being too small, causing the image to shrink to 0
|
||||
if (scaleFactor < 0.2 || previousFileSize == currentSize) {
|
||||
throw new RuntimeException("Could not reach the desired size without excessively degrading image quality, lowest size recommended is " + FileUtils.byteCountToDisplaySize(currentSize) + ", " + currentSize + " bytes");
|
||||
throw new RuntimeException(
|
||||
"Could not reach the desired size without excessively degrading image quality, lowest size recommended is "
|
||||
+ FileUtils.byteCountToDisplaySize(currentSize)
|
||||
+ ", "
|
||||
+ currentSize
|
||||
+ " bytes");
|
||||
}
|
||||
previousFileSize = currentSize;
|
||||
} else {
|
||||
@ -211,10 +240,7 @@ public class CompressController {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -224,7 +250,8 @@ public class CompressController {
|
||||
// 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.");
|
||||
logger.warn(
|
||||
"Optimized file is larger than the original. Returning the original file instead.");
|
||||
|
||||
// Read the original file again
|
||||
pdfBytes = Files.readAllBytes(tempInputFile);
|
||||
@ -235,8 +262,8 @@ public class CompressController {
|
||||
Files.delete(tempOutputFile);
|
||||
|
||||
// Return the optimized PDF as a response
|
||||
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_Optimized.pdf";
|
||||
String outputFilename =
|
||||
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_Optimized.pdf";
|
||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -32,10 +32,12 @@ import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
@ -44,18 +46,28 @@ 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. Input:PDF Output:IMAGE/ZIP Type:SIMO")
|
||||
@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. Input:PDF Output:IMAGE/ZIP Type:SIMO")
|
||||
public ResponseEntity<byte[]> extractImageScans(
|
||||
@RequestBody(
|
||||
description = "Form data containing file and extraction parameters",
|
||||
required = true,
|
||||
content = @Content(
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "multipart/form-data",
|
||||
schema = @Schema(implementation = ExtractImageScansRequest.class) // This should represent your form's structure
|
||||
)
|
||||
)
|
||||
ExtractImageScansRequest form) throws IOException, InterruptedException {
|
||||
schema =
|
||||
@Schema(
|
||||
implementation =
|
||||
ExtractImageScansRequest
|
||||
.class) // This should
|
||||
// represent
|
||||
// your form's
|
||||
// structure
|
||||
))
|
||||
ExtractImageScansRequest form)
|
||||
throws IOException, InterruptedException {
|
||||
String fileName = form.getFileInput().getOriginalFilename();
|
||||
String extension = fileName.substring(fileName.lastIndexOf(".") + 1);
|
||||
|
||||
@ -64,7 +76,8 @@ public class ExtractImageScansController {
|
||||
// Check if input file is a PDF
|
||||
if (extension.equalsIgnoreCase("pdf")) {
|
||||
// Load PDF document
|
||||
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) {
|
||||
try (PDDocument document =
|
||||
PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) {
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
int pageCount = document.getNumberOfPages();
|
||||
images = new ArrayList<>();
|
||||
@ -84,7 +97,10 @@ public class ExtractImageScansController {
|
||||
}
|
||||
} else {
|
||||
Path tempInputFile = Files.createTempFile("input_", "." + extension);
|
||||
Files.copy(form.getFileInput().getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
Files.copy(
|
||||
form.getFileInput().getInputStream(),
|
||||
tempInputFile,
|
||||
StandardCopyOption.REPLACE_EXISTING);
|
||||
// Add input file path to images list
|
||||
images.add(tempInputFile.toString());
|
||||
}
|
||||
@ -95,21 +111,28 @@ public class ExtractImageScansController {
|
||||
for (int i = 0; i < images.size(); i++) {
|
||||
|
||||
Path tempDir = Files.createTempDirectory("openCV_output");
|
||||
List<String> command = new ArrayList<>(Arrays.asList(
|
||||
List<String> command =
|
||||
new ArrayList<>(
|
||||
Arrays.asList(
|
||||
"python3",
|
||||
"./scripts/split_photos.py",
|
||||
images.get(i),
|
||||
tempDir.toString(),
|
||||
"--angle_threshold", String.valueOf(form.getAngleThreshold()),
|
||||
"--tolerance", String.valueOf(form.getTolerance()),
|
||||
"--min_area", String.valueOf(form.getMinArea()),
|
||||
"--min_contour_area", String.valueOf(form.getMinContourArea()),
|
||||
"--border_size", String.valueOf(form.getBorderSize())
|
||||
));
|
||||
|
||||
"--angle_threshold",
|
||||
String.valueOf(form.getAngleThreshold()),
|
||||
"--tolerance",
|
||||
String.valueOf(form.getTolerance()),
|
||||
"--min_area",
|
||||
String.valueOf(form.getMinArea()),
|
||||
"--min_contour_area",
|
||||
String.valueOf(form.getMinContourArea()),
|
||||
"--border_size",
|
||||
String.valueOf(form.getBorderSize())));
|
||||
|
||||
// Run CLI command
|
||||
ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV).runCommandWithOutputHandling(command);
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
// Read the output photos in temp directory
|
||||
List<Path> tempOutputFiles = Files.list(tempDir).sorted().collect(Collectors.toList());
|
||||
@ -126,10 +149,16 @@ public class ExtractImageScansController {
|
||||
String outputZipFilename = fileName.replaceFirst("[.][^.]+$", "") + "_processed.zip";
|
||||
Path tempZipFile = Files.createTempFile("output_", ".zip");
|
||||
|
||||
try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) {
|
||||
try (ZipOutputStream zipOut =
|
||||
new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) {
|
||||
// Add processed images to the zip
|
||||
for (int i = 0; i < processedImageBytes.size(); i++) {
|
||||
ZipEntry entry = new ZipEntry(fileName.replaceFirst("[.][^.]+$", "") + "_" + (i + 1) + ".png");
|
||||
ZipEntry entry =
|
||||
new ZipEntry(
|
||||
fileName.replaceFirst("[.][^.]+$", "")
|
||||
+ "_"
|
||||
+ (i + 1)
|
||||
+ ".png");
|
||||
zipOut.putNextEntry(entry);
|
||||
zipOut.write(processedImageBytes.get(i));
|
||||
zipOut.closeEntry();
|
||||
@ -141,13 +170,15 @@ public class ExtractImageScansController {
|
||||
// Clean up the temporary zip file
|
||||
Files.delete(tempZipFile);
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
|
||||
} else {
|
||||
// Return the processed image as a response
|
||||
byte[] imageBytes = processedImageBytes.get(0);
|
||||
return WebResponseUtils.bytesToWebResponse(imageBytes, fileName.replaceFirst("[.][^.]+$", "") + ".png", MediaType.IMAGE_PNG);
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
imageBytes,
|
||||
fileName.replaceFirst("[.][^.]+$", "") + ".png",
|
||||
MediaType.IMAGE_PNG);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
package stirling.software.SPDF.controller.api.misc;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Image;
|
||||
import java.awt.image.BufferedImage;
|
||||
@ -29,8 +30,10 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.PDFWithImageFormatRequest;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
@ -39,13 +42,17 @@ 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. Input:PDF Output:IMAGE/ZIP Type:SIMO")
|
||||
public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFWithImageFormatRequest request) throws IOException {
|
||||
@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. Input:PDF Output:IMAGE/ZIP Type:SIMO")
|
||||
public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFWithImageFormatRequest request)
|
||||
throws IOException {
|
||||
MultipartFile file = request.getFileInput();
|
||||
String format = request.getFormat();
|
||||
|
||||
System.out.println(System.currentTimeMillis() + "file=" + file.getName() + ", format=" + format);
|
||||
System.out.println(
|
||||
System.currentTimeMillis() + "file=" + file.getName() + ", format=" + format);
|
||||
PDDocument document = PDDocument.load(file.getBytes());
|
||||
|
||||
// Create ByteArrayOutputStream to write zip file to byte array
|
||||
@ -78,15 +85,28 @@ public class ExtractImagesController {
|
||||
RenderedImage renderedImage = image.getImage();
|
||||
BufferedImage bufferedImage = null;
|
||||
if (format.equalsIgnoreCase("png")) {
|
||||
bufferedImage = new BufferedImage(renderedImage.getWidth(), renderedImage.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
bufferedImage =
|
||||
new BufferedImage(
|
||||
renderedImage.getWidth(),
|
||||
renderedImage.getHeight(),
|
||||
BufferedImage.TYPE_INT_ARGB);
|
||||
} else if (format.equalsIgnoreCase("jpeg") || format.equalsIgnoreCase("jpg")) {
|
||||
bufferedImage = new BufferedImage(renderedImage.getWidth(), renderedImage.getHeight(), BufferedImage.TYPE_INT_RGB);
|
||||
bufferedImage =
|
||||
new BufferedImage(
|
||||
renderedImage.getWidth(),
|
||||
renderedImage.getHeight(),
|
||||
BufferedImage.TYPE_INT_RGB);
|
||||
} else if (format.equalsIgnoreCase("gif")) {
|
||||
bufferedImage = new BufferedImage(renderedImage.getWidth(), renderedImage.getHeight(), BufferedImage.TYPE_BYTE_INDEXED);
|
||||
bufferedImage =
|
||||
new BufferedImage(
|
||||
renderedImage.getWidth(),
|
||||
renderedImage.getHeight(),
|
||||
BufferedImage.TYPE_BYTE_INDEXED);
|
||||
}
|
||||
|
||||
// Write image to zip file
|
||||
String imageName = filename + "_" + imageIndex + " (Page " + pageNum + ")." + format;
|
||||
String imageName =
|
||||
filename + "_" + imageIndex + " (Page " + pageNum + ")." + format;
|
||||
ZipEntry zipEntry = new ZipEntry(imageName);
|
||||
zos.putNextEntry(zipEntry);
|
||||
|
||||
@ -111,7 +131,7 @@ public class ExtractImagesController {
|
||||
// Create ByteArrayResource from byte array
|
||||
byte[] zipContents = baos.toByteArray();
|
||||
|
||||
return WebResponseUtils.boasToWebResponse(baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
return WebResponseUtils.boasToWebResponse(
|
||||
baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,21 +3,17 @@ package stirling.software.SPDF.controller.api.misc;
|
||||
import java.awt.Color;
|
||||
import java.awt.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;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
//Required for file input/output
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
//Other required classes
|
||||
import java.util.Random;
|
||||
|
||||
//Required for image input/output
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
@ -40,6 +36,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.PDFFile;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@ -55,15 +52,14 @@ public class FakeScanControllerWIP {
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/fakeScan")
|
||||
@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.")
|
||||
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request) throws IOException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
|
||||
PDDocument document = PDDocument.load(inputFile.getBytes());
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
for (int page = 0; page < document.getNumberOfPages(); ++page)
|
||||
{
|
||||
for (int page = 0; page < document.getNumberOfPages(); ++page) {
|
||||
BufferedImage image = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
|
||||
ImageIO.write(image, "png", new File("scanned-" + (page + 1) + ".png"));
|
||||
}
|
||||
@ -77,7 +73,9 @@ public class FakeScanControllerWIP {
|
||||
BufferedImage sourceImage = ImageIO.read(new File("scanned-1.png"));
|
||||
|
||||
// Create the destination image
|
||||
BufferedImage destinationImage = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), sourceImage.getType());
|
||||
BufferedImage destinationImage =
|
||||
new BufferedImage(
|
||||
sourceImage.getWidth(), sourceImage.getHeight(), sourceImage.getType());
|
||||
|
||||
// Apply a brightness and contrast effect based on the "scanned-ness"
|
||||
float scaleFactor = 1.0f + (scannedness / 100.0f) * 0.5f; // Between 1.0 and 1.5
|
||||
@ -86,10 +84,14 @@ public class FakeScanControllerWIP {
|
||||
op.filter(sourceImage, destinationImage);
|
||||
|
||||
// Apply a rotation effect
|
||||
double rotationRequired = Math.toRadians((new SecureRandom().nextInt(3 - 1) + 1)); // Random angle between 1 and 3 degrees
|
||||
double rotationRequired =
|
||||
Math.toRadians(
|
||||
(new SecureRandom().nextInt(3 - 1)
|
||||
+ 1)); // Random angle between 1 and 3 degrees
|
||||
double locationX = destinationImage.getWidth() / 2;
|
||||
double locationY = destinationImage.getHeight() / 2;
|
||||
AffineTransform tx = AffineTransform.getRotateInstance(rotationRequired, locationX, locationY);
|
||||
AffineTransform tx =
|
||||
AffineTransform.getRotateInstance(rotationRequired, locationX, locationY);
|
||||
AffineTransformOp rotateOp = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR);
|
||||
destinationImage = rotateOp.filter(destinationImage, null);
|
||||
|
||||
@ -100,7 +102,8 @@ public class FakeScanControllerWIP {
|
||||
blurIntensity, blurIntensity, blurIntensity,
|
||||
blurIntensity, blurIntensity, blurIntensity
|
||||
};
|
||||
BufferedImageOp blurOp = new ConvolveOp(new Kernel(3, 3, matrix), ConvolveOp.EDGE_NO_OP, null);
|
||||
BufferedImageOp blurOp =
|
||||
new ConvolveOp(new Kernel(3, 3, matrix), ConvolveOp.EDGE_NO_OP, null);
|
||||
destinationImage = blurOp.filter(destinationImage, null);
|
||||
|
||||
// Add noise to the image based on the "dirtiness"
|
||||
@ -117,15 +120,8 @@ public class FakeScanControllerWIP {
|
||||
// Save the image
|
||||
ImageIO.write(destinationImage, "PNG", new File("scanned-1.png"));
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
PDDocument documentOut = new PDDocument();
|
||||
for (int page = 1; page <= document.getNumberOfPages(); ++page)
|
||||
{
|
||||
for (int page = 1; page <= document.getNumberOfPages(); ++page) {
|
||||
BufferedImage bim = ImageIO.read(new File("scanned-" + page + ".png"));
|
||||
|
||||
// Adjust the dimensions of the page
|
||||
@ -144,8 +140,8 @@ public class FakeScanControllerWIP {
|
||||
documentOut.close();
|
||||
|
||||
// Return the optimized PDF as a response
|
||||
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scanned.pdf";
|
||||
String outputFilename =
|
||||
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scanned.pdf";
|
||||
return WebResponseUtils.boasToWebResponse(baos, outputFilename);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.MetadataRequest;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@ -27,7 +28,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
public class MetadataController {
|
||||
|
||||
|
||||
private String checkUndefined(String entry) {
|
||||
// Check if the string is "undefined"
|
||||
if ("undefined".equals(entry)) {
|
||||
@ -36,13 +36,15 @@ public class MetadataController {
|
||||
}
|
||||
// Return the original string if it's not "undefined"
|
||||
return entry;
|
||||
|
||||
}
|
||||
|
||||
@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. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> metadata(@ModelAttribute MetadataRequest request) throws IOException {
|
||||
@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. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> metadata(@ModelAttribute MetadataRequest request)
|
||||
throws IOException {
|
||||
|
||||
// Extract PDF file from the request object
|
||||
MultipartFile pdfFile = request.getFileInput();
|
||||
@ -89,7 +91,9 @@ public class MetadataController {
|
||||
}
|
||||
// Remove metadata from the PDF history
|
||||
document.getDocumentCatalog().getCOSObject().removeItem(COSName.getPDFName("Metadata"));
|
||||
document.getDocumentCatalog().getCOSObject().removeItem(COSName.getPDFName("PieceInfo"));
|
||||
document.getDocumentCatalog()
|
||||
.getCOSObject()
|
||||
.removeItem(COSName.getPDFName("PieceInfo"));
|
||||
author = null;
|
||||
creationDate = null;
|
||||
creator = null;
|
||||
@ -104,9 +108,17 @@ public class MetadataController {
|
||||
for (Entry<String, String> entry : allRequestParams.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
// Check if the key is a standard metadata key
|
||||
if (!key.equalsIgnoreCase("Author") && !key.equalsIgnoreCase("CreationDate") && !key.equalsIgnoreCase("Creator") && !key.equalsIgnoreCase("Keywords")
|
||||
&& !key.equalsIgnoreCase("modificationDate") && !key.equalsIgnoreCase("Producer") && !key.equalsIgnoreCase("Subject") && !key.equalsIgnoreCase("Title")
|
||||
&& !key.equalsIgnoreCase("Trapped") && !key.contains("customKey") && !key.contains("customValue")) {
|
||||
if (!key.equalsIgnoreCase("Author")
|
||||
&& !key.equalsIgnoreCase("CreationDate")
|
||||
&& !key.equalsIgnoreCase("Creator")
|
||||
&& !key.equalsIgnoreCase("Keywords")
|
||||
&& !key.equalsIgnoreCase("modificationDate")
|
||||
&& !key.equalsIgnoreCase("Producer")
|
||||
&& !key.equalsIgnoreCase("Subject")
|
||||
&& !key.equalsIgnoreCase("Title")
|
||||
&& !key.equalsIgnoreCase("Trapped")
|
||||
&& !key.contains("customKey")
|
||||
&& !key.contains("customValue")) {
|
||||
info.setCustomMetadataValue(key, entry.getValue());
|
||||
} else if (key.contains("customKey")) {
|
||||
int number = Integer.parseInt(key.replaceAll("\\D", ""));
|
||||
@ -119,7 +131,8 @@ public class MetadataController {
|
||||
if (creationDate != null && creationDate.length() > 0) {
|
||||
Calendar creationDateCal = Calendar.getInstance();
|
||||
try {
|
||||
creationDateCal.setTime(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(creationDate));
|
||||
creationDateCal.setTime(
|
||||
new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(creationDate));
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@ -130,7 +143,8 @@ public class MetadataController {
|
||||
if (modificationDate != null && modificationDate.length() > 0) {
|
||||
Calendar modificationDateCal = Calendar.getInstance();
|
||||
try {
|
||||
modificationDateCal.setTime(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(modificationDate));
|
||||
modificationDateCal.setTime(
|
||||
new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(modificationDate));
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@ -147,7 +161,8 @@ public class MetadataController {
|
||||
info.setTrapped(trapped);
|
||||
|
||||
document.setDocumentInformation(info);
|
||||
return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_metadata.pdf");
|
||||
return WebResponseUtils.pdfDocToWebResponse(
|
||||
document,
|
||||
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_metadata.pdf");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||
@ -44,14 +45,21 @@ public class OCRController {
|
||||
if (files == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Arrays.stream(files).filter(file -> file.getName().endsWith(".traineddata")).map(file -> file.getName().replace(".traineddata", ""))
|
||||
.filter(lang -> !lang.equalsIgnoreCase("osd")).collect(Collectors.toList());
|
||||
return Arrays.stream(files)
|
||||
.filter(file -> file.getName().endsWith(".traineddata"))
|
||||
.map(file -> file.getName().replace(".traineddata", ""))
|
||||
.filter(lang -> !lang.equalsIgnoreCase("osd"))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
|
||||
@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. Input:PDF Output:PDF Type:SI-Conditional")
|
||||
public ResponseEntity<byte[]> processPdfWithOCR(@ModelAttribute ProcessPdfWithOcrRequest request) throws IOException, InterruptedException {
|
||||
@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. Input:PDF Output:PDF Type:SI-Conditional")
|
||||
public ResponseEntity<byte[]> processPdfWithOCR(
|
||||
@ModelAttribute ProcessPdfWithOcrRequest request)
|
||||
throws IOException, InterruptedException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
List<String> selectedLanguages = request.getLanguages();
|
||||
Boolean sidecar = request.isSidecar();
|
||||
@ -74,7 +82,8 @@ public class OCRController {
|
||||
List<String> availableLanguages = getAvailableTesseractLanguages();
|
||||
|
||||
// Validate selected languages
|
||||
selectedLanguages = selectedLanguages.stream().filter(availableLanguages::contains).toList();
|
||||
selectedLanguages =
|
||||
selectedLanguages.stream().filter(availableLanguages::contains).toList();
|
||||
|
||||
if (selectedLanguages.isEmpty()) {
|
||||
throw new IOException("None of the selected languages are valid.");
|
||||
@ -92,8 +101,16 @@ public class OCRController {
|
||||
// Run OCR Command
|
||||
String languageOption = String.join("+", selectedLanguages);
|
||||
|
||||
|
||||
List<String> command = new ArrayList<>(Arrays.asList("ocrmypdf", "--verbose", "2", "--output-type", "pdf", "--pdf-renderer" , ocrRenderType));
|
||||
List<String> command =
|
||||
new ArrayList<>(
|
||||
Arrays.asList(
|
||||
"ocrmypdf",
|
||||
"--verbose",
|
||||
"2",
|
||||
"--output-type",
|
||||
"pdf",
|
||||
"--pdf-renderer",
|
||||
ocrRenderType));
|
||||
|
||||
if (sidecar != null && sidecar) {
|
||||
sidecarTextPath = Files.createTempFile("sidecar", ".txt");
|
||||
@ -120,26 +137,42 @@ public class OCRController {
|
||||
}
|
||||
}
|
||||
|
||||
command.addAll(Arrays.asList("--language", languageOption, tempInputFile.toString(), tempOutputFile.toString()));
|
||||
command.addAll(
|
||||
Arrays.asList(
|
||||
"--language",
|
||||
languageOption,
|
||||
tempInputFile.toString(),
|
||||
tempOutputFile.toString()));
|
||||
|
||||
// Run CLI command
|
||||
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")) {
|
||||
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);
|
||||
result =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
|
||||
.runCommandWithOutputHandling(command);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Remove images from the OCR processed PDF if the flag is set to true
|
||||
if (removeImagesAfter != null && removeImagesAfter) {
|
||||
Path tempPdfWithoutImages = Files.createTempFile("output_", "_no_images.pdf");
|
||||
|
||||
List<String> gsCommand = Arrays.asList("gs", "-sDEVICE=pdfwrite", "-dFILTERIMAGE", "-o", tempPdfWithoutImages.toString(), tempOutputFile.toString());
|
||||
List<String> gsCommand =
|
||||
Arrays.asList(
|
||||
"gs",
|
||||
"-sDEVICE=pdfwrite",
|
||||
"-dFILTERIMAGE",
|
||||
"-o",
|
||||
tempPdfWithoutImages.toString(),
|
||||
tempOutputFile.toString());
|
||||
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(gsCommand);
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||
.runCommandWithOutputHandling(gsCommand);
|
||||
tempOutputFile = tempPdfWithoutImages;
|
||||
}
|
||||
// Read the OCR processed PDF file
|
||||
@ -148,14 +181,17 @@ public class OCRController {
|
||||
Files.delete(tempInputFile);
|
||||
|
||||
// Return the OCR processed PDF as a response
|
||||
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.pdf";
|
||||
String outputFilename =
|
||||
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.pdf";
|
||||
|
||||
if (sidecar != null && sidecar) {
|
||||
// Create a zip file containing both the PDF and the text file
|
||||
String outputZipFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.zip";
|
||||
String outputZipFilename =
|
||||
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.zip";
|
||||
Path tempZipFile = Files.createTempFile("output_", ".zip");
|
||||
|
||||
try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) {
|
||||
try (ZipOutputStream zipOut =
|
||||
new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) {
|
||||
// Add PDF file to the zip
|
||||
ZipEntry pdfEntry = new ZipEntry(outputFilename);
|
||||
zipOut.putNextEntry(pdfEntry);
|
||||
@ -177,13 +213,12 @@ public class OCRController {
|
||||
Files.delete(sidecarTextPath);
|
||||
|
||||
// Return the zip file containing both the PDF and the text file
|
||||
return WebResponseUtils.bytesToWebResponse(zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
|
||||
} else {
|
||||
// Return the OCR processed PDF as a response
|
||||
Files.delete(tempOutputFile);
|
||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.OverlayImageRequest;
|
||||
import stirling.software.SPDF.utils.PdfUtils;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@ -28,8 +29,8 @@ 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. Input:PDF/IMAGE Output:PDF Type:MF-SISO"
|
||||
)
|
||||
description =
|
||||
"This endpoint overlays an image onto a PDF file at the specified coordinates. The image can be overlaid on every page of the PDF if specified. Input:PDF/IMAGE Output:PDF Type:MF-SISO")
|
||||
public ResponseEntity<byte[]> overlayImage(@ModelAttribute OverlayImageRequest request) {
|
||||
MultipartFile pdfFile = request.getFileInput();
|
||||
MultipartFile imageFile = request.getImageFile();
|
||||
@ -41,7 +42,9 @@ public class OverlayImageController {
|
||||
byte[] imageBytes = imageFile.getBytes();
|
||||
byte[] result = PdfUtils.overlayImage(pdfBytes, imageBytes, x, y, everyPage);
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(result, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf");
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
result,
|
||||
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf");
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to add image to PDF", e);
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
|
@ -21,6 +21,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.AddPageNumbersRequest;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@ -33,8 +34,12 @@ 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(@ModelAttribute AddPageNumbersRequest request) throws IOException {
|
||||
@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(@ModelAttribute AddPageNumbersRequest request)
|
||||
throws IOException {
|
||||
MultipartFile file = request.getFileInput();
|
||||
String customMargin = request.getCustomMargin();
|
||||
int position = request.getPosition();
|
||||
@ -60,7 +65,6 @@ public class PageNumbersController {
|
||||
marginFactor = 0.075f;
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
marginFactor = 0.035f;
|
||||
break;
|
||||
@ -74,13 +78,23 @@ public class PageNumbersController {
|
||||
if (customText == null || customText.length() == 0) {
|
||||
customText = "{n}";
|
||||
}
|
||||
List<Integer> pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages());
|
||||
List<Integer> pagesToNumberList =
|
||||
GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages());
|
||||
|
||||
for (int i : pagesToNumberList) {
|
||||
PDPage page = document.getPage(i);
|
||||
PDRectangle pageSize = page.getMediaBox();
|
||||
|
||||
String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(document.getNumberOfPages())).replace("{filename}", file.getOriginalFilename().replaceFirst("[.][^.]+$", "")) : String.valueOf(pageNumber);
|
||||
String text =
|
||||
customText != null
|
||||
? customText
|
||||
.replace("{n}", String.valueOf(pageNumber))
|
||||
.replace("{total}", String.valueOf(document.getNumberOfPages()))
|
||||
.replace(
|
||||
"{filename}",
|
||||
file.getOriginalFilename()
|
||||
.replaceFirst("[.][^.]+$", ""))
|
||||
: String.valueOf(pageNumber);
|
||||
|
||||
float x, y;
|
||||
|
||||
@ -111,7 +125,9 @@ public class PageNumbersController {
|
||||
break;
|
||||
}
|
||||
|
||||
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true);
|
||||
PDPageContentStream contentStream =
|
||||
new PDPageContentStream(
|
||||
document, page, PDPageContentStream.AppendMode.APPEND, true);
|
||||
contentStream.beginText();
|
||||
contentStream.setFont(font, fontSize);
|
||||
contentStream.newLineAtOffset(x, y);
|
||||
@ -126,10 +142,9 @@ public class PageNumbersController {
|
||||
document.save(baos);
|
||||
document.close();
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", MediaType.APPLICATION_PDF);
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
baos.toByteArray(),
|
||||
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf",
|
||||
MediaType.APPLICATION_PDF);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.PDFFile;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||
@ -32,9 +33,10 @@ 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. Input:PDF Output:PDF Type:SISO"
|
||||
)
|
||||
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request) throws IOException, InterruptedException {
|
||||
description =
|
||||
"This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request)
|
||||
throws IOException, InterruptedException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
// Save the uploaded file to a temporary location
|
||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||
@ -50,8 +52,9 @@ public class RepairController {
|
||||
command.add("-sDEVICE=pdfwrite");
|
||||
command.add(tempInputFile.toString());
|
||||
|
||||
|
||||
ProcessExecutorResult 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);
|
||||
@ -61,8 +64,8 @@ public class RepairController {
|
||||
Files.delete(tempOutputFile);
|
||||
|
||||
// Return the optimized PDF as a response
|
||||
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_repaired.pdf";
|
||||
String outputFilename =
|
||||
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_repaired.pdf";
|
||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -17,24 +17,31 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.PDFFile;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
public class ShowJavascript {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class);
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/show-javascript")
|
||||
@Operation(summary = "Grabs all JS from a PDF and returns a single JS file with all code", description = "desc. Input:PDF Output:JS Type:SISO")
|
||||
@Operation(
|
||||
summary = "Grabs all JS from a PDF and returns a single JS file with all code",
|
||||
description = "desc. Input:PDF Output:JS Type:SISO")
|
||||
public ResponseEntity<byte[]> extractHeader(@ModelAttribute PDFFile request) throws Exception {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
String script = "";
|
||||
|
||||
try (PDDocument document = PDDocument.load(inputFile.getInputStream())) {
|
||||
|
||||
if(document.getDocumentCatalog() != null && document.getDocumentCatalog().getNames() != null) {
|
||||
PDNameTreeNode<PDActionJavaScript> jsTree = document.getDocumentCatalog().getNames().getJavaScript();
|
||||
if (document.getDocumentCatalog() != null
|
||||
&& document.getDocumentCatalog().getNames() != null) {
|
||||
PDNameTreeNode<PDActionJavaScript> jsTree =
|
||||
document.getDocumentCatalog().getNames().getJavaScript();
|
||||
|
||||
if (jsTree != null) {
|
||||
Map<String, PDActionJavaScript> jsEntries = jsTree.getNames();
|
||||
@ -44,20 +51,26 @@ public class ShowJavascript {
|
||||
PDActionJavaScript jsAction = entry.getValue();
|
||||
String jsCodeStr = jsAction.getAction();
|
||||
|
||||
script += "// File: " + inputFile.getOriginalFilename() + ", Script: " + name + "\n" + jsCodeStr + "\n";
|
||||
script +=
|
||||
"// File: "
|
||||
+ inputFile.getOriginalFilename()
|
||||
+ ", Script: "
|
||||
+ name
|
||||
+ "\n"
|
||||
+ jsCodeStr
|
||||
+ "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (script.isEmpty()) {
|
||||
script = "PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript";
|
||||
script =
|
||||
"PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript";
|
||||
}
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(script.getBytes(StandardCharsets.UTF_8), inputFile.getOriginalFilename() + ".js");
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
script.getBytes(StandardCharsets.UTF_8),
|
||||
inputFile.getOriginalFilename() + ".js");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
package stirling.software.SPDF.controller.api.pipeline;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpEntity;
|
||||
@ -17,11 +19,10 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import jakarta.servlet.ServletContext;
|
||||
|
||||
import stirling.software.SPDF.SPdfApplication;
|
||||
import stirling.software.SPDF.model.ApiEndpoint;
|
||||
import stirling.software.SPDF.model.Role;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
@Service
|
||||
public class ApiDocService {
|
||||
|
||||
@ -29,8 +30,7 @@ public class ApiDocService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ApiDocService.class);
|
||||
|
||||
@Autowired
|
||||
private ServletContext servletContext;
|
||||
@Autowired private ServletContext servletContext;
|
||||
|
||||
private String getApiDocsUrl() {
|
||||
String contextPath = servletContext.getContextPath();
|
||||
@ -39,19 +39,16 @@ public class ApiDocService {
|
||||
return "http://localhost:" + port + contextPath + "/v1/api-docs";
|
||||
}
|
||||
|
||||
|
||||
@Autowired(required = false)
|
||||
private UserServiceInterface userService;
|
||||
|
||||
private String getApiKeyForUser() {
|
||||
if(userService == null)
|
||||
return "";
|
||||
if (userService == null) return "";
|
||||
return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId());
|
||||
}
|
||||
|
||||
JsonNode apiDocsJsonRootNode;
|
||||
|
||||
|
||||
// @EventListener(ApplicationReadyEvent.class)
|
||||
private synchronized void loadApiDocumentation() {
|
||||
String apiDocsJson = "";
|
||||
@ -64,14 +61,17 @@ public class ApiDocService {
|
||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
ResponseEntity<String> response = restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class);
|
||||
ResponseEntity<String> response =
|
||||
restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class);
|
||||
apiDocsJson = response.getBody();
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
apiDocsJsonRootNode = mapper.readTree(apiDocsJson);
|
||||
|
||||
JsonNode paths = apiDocsJsonRootNode.path("paths");
|
||||
paths.fields().forEachRemaining(entry -> {
|
||||
paths.fields()
|
||||
.forEachRemaining(
|
||||
entry -> {
|
||||
String path = entry.getKey();
|
||||
JsonNode pathNode = entry.getValue();
|
||||
if (pathNode.has("post")) {
|
||||
@ -120,4 +120,3 @@ public class ApiDocService {
|
||||
}
|
||||
|
||||
// Model class for API Endpoint
|
||||
|
||||
|
@ -2,7 +2,9 @@ package stirling.software.SPDF.controller.api.pipeline;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
@ -24,6 +26,7 @@ import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
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.api.HandleDataRequest;
|
||||
@ -38,19 +41,15 @@ public class PipelineController {
|
||||
|
||||
final String watchedFoldersDir = "./pipeline/watchedFolders/";
|
||||
final String finishedFoldersDir = "./pipeline/finishedFolders/";
|
||||
@Autowired
|
||||
PipelineProcessor processor;
|
||||
@Autowired PipelineProcessor processor;
|
||||
|
||||
@Autowired ApplicationProperties applicationProperties;
|
||||
|
||||
@Autowired
|
||||
ApplicationProperties applicationProperties;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired private ObjectMapper objectMapper;
|
||||
|
||||
@PostMapping("/handleData")
|
||||
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request) throws JsonMappingException, JsonProcessingException {
|
||||
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request)
|
||||
throws JsonMappingException, JsonProcessingException {
|
||||
if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) {
|
||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
@ -77,8 +76,8 @@ public class PipelineController {
|
||||
is.close();
|
||||
|
||||
logger.info("Returning single file response...");
|
||||
return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(),
|
||||
MediaType.APPLICATION_OCTET_STREAM);
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM);
|
||||
} else if (outputFiles == null) {
|
||||
return null;
|
||||
}
|
||||
@ -87,9 +86,26 @@ public class PipelineController {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ZipOutputStream zipOut = new ZipOutputStream(baos);
|
||||
|
||||
// A map to keep track of filenames and their counts
|
||||
Map<String, Integer> filenameCount = new HashMap<>();
|
||||
|
||||
// Loop through each file and add it to the zip
|
||||
for (Resource file : outputFiles) {
|
||||
ZipEntry zipEntry = new ZipEntry(file.getFilename());
|
||||
String originalFilename = file.getFilename();
|
||||
String filename = originalFilename;
|
||||
|
||||
// Check if the filename already exists, and modify it if necessary
|
||||
if (filenameCount.containsKey(originalFilename)) {
|
||||
int count = filenameCount.get(originalFilename);
|
||||
String baseName = originalFilename.replaceAll("\\.[^.]*$", "");
|
||||
String extension = originalFilename.replaceAll("^.*\\.", "");
|
||||
filename = baseName + "(" + count + ")." + extension;
|
||||
filenameCount.put(originalFilename, count + 1);
|
||||
} else {
|
||||
filenameCount.put(originalFilename, 1);
|
||||
}
|
||||
|
||||
ZipEntry zipEntry = new ZipEntry(filename);
|
||||
zipOut.putNextEntry(zipEntry);
|
||||
|
||||
// Read the file into a byte array
|
||||
@ -107,11 +123,11 @@ public class PipelineController {
|
||||
zipOut.close();
|
||||
|
||||
logger.info("Returning zipped file response...");
|
||||
return WebResponseUtils.boasToWebResponse(baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
return WebResponseUtils.boasToWebResponse(
|
||||
baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||
} catch (Exception e) {
|
||||
logger.error("Error handling data: ", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -34,18 +34,14 @@ import stirling.software.SPDF.model.PipelineOperation;
|
||||
public class PipelineDirectoryProcessor {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PipelineDirectoryProcessor.class);
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
@Autowired
|
||||
private ApiDocService apiDocService;
|
||||
@Autowired
|
||||
private ApplicationProperties applicationProperties;
|
||||
@Autowired private ObjectMapper objectMapper;
|
||||
@Autowired private ApiDocService apiDocService;
|
||||
@Autowired private ApplicationProperties applicationProperties;
|
||||
|
||||
final String watchedFoldersDir = "./pipeline/watchedFolders/";
|
||||
final String finishedFoldersDir = "./pipeline/finishedFolders/";
|
||||
|
||||
@Autowired
|
||||
PipelineProcessor processor;
|
||||
@Autowired PipelineProcessor processor;
|
||||
|
||||
@Scheduled(fixedRate = 60000)
|
||||
public void scanFolders() {
|
||||
@ -63,7 +59,9 @@ public class PipelineDirectoryProcessor {
|
||||
}
|
||||
}
|
||||
try (Stream<Path> paths = Files.walk(watchedFolderPath)) {
|
||||
paths.filter(Files::isDirectory).forEach(t -> {
|
||||
paths.filter(Files::isDirectory)
|
||||
.forEach(
|
||||
t -> {
|
||||
try {
|
||||
if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) {
|
||||
handleDirectory(t);
|
||||
@ -113,7 +111,8 @@ public class PipelineDirectoryProcessor {
|
||||
return objectMapper.readValue(jsonString, PipelineConfig.class);
|
||||
}
|
||||
|
||||
private void processPipelineOperations(Path dir, Path processingDir, Path jsonFile, PipelineConfig config) throws IOException {
|
||||
private void processPipelineOperations(
|
||||
Path dir, Path processingDir, Path jsonFile, PipelineConfig config) throws IOException {
|
||||
for (PipelineOperation operation : config.getOperations()) {
|
||||
validateOperation(operation);
|
||||
File[] files = collectFilesForProcessing(dir, jsonFile, operation);
|
||||
@ -132,7 +131,8 @@ public class PipelineDirectoryProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
private File[] collectFilesForProcessing(Path dir, Path jsonFile, PipelineOperation operation) throws IOException {
|
||||
private File[] collectFilesForProcessing(Path dir, Path jsonFile, PipelineOperation operation)
|
||||
throws IOException {
|
||||
try (Stream<Path> paths = Files.list(dir)) {
|
||||
if ("automated".equals(operation.getParameters().get("fileInput"))) {
|
||||
return paths.filter(path -> !Files.isDirectory(path) && !path.equals(jsonFile))
|
||||
@ -145,7 +145,8 @@ public class PipelineDirectoryProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
private List<File> prepareFilesForProcessing(File[] files, Path processingDir) throws IOException {
|
||||
private List<File> prepareFilesForProcessing(File[] files, Path processingDir)
|
||||
throws IOException {
|
||||
List<File> filesToProcess = new ArrayList<>();
|
||||
for (File file : files) {
|
||||
Path targetPath = resolveUniqueFilePath(processingDir, file.getName());
|
||||
@ -173,13 +174,18 @@ public class PipelineDirectoryProcessor {
|
||||
if (dotIndex == -1) {
|
||||
return originalFileName + suffix;
|
||||
} else {
|
||||
return originalFileName.substring(0, dotIndex) + suffix + originalFileName.substring(dotIndex);
|
||||
return originalFileName.substring(0, dotIndex)
|
||||
+ suffix
|
||||
+ originalFileName.substring(dotIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private void runPipelineAgainstFiles(List<File> filesToProcess, PipelineConfig config, Path dir, Path processingDir) throws IOException {
|
||||
private void runPipelineAgainstFiles(
|
||||
List<File> filesToProcess, PipelineConfig config, Path dir, Path processingDir)
|
||||
throws IOException {
|
||||
try {
|
||||
List<Resource> inputFiles = processor.generateInputFiles(filesToProcess.toArray(new File[0]));
|
||||
List<Resource> inputFiles =
|
||||
processor.generateInputFiles(filesToProcess.toArray(new File[0]));
|
||||
if (inputFiles == null || inputFiles.size() == 0) {
|
||||
return;
|
||||
}
|
||||
@ -193,7 +199,8 @@ public class PipelineDirectoryProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
private void moveAndRenameFiles(List<Resource> resources, PipelineConfig config, Path dir) throws IOException {
|
||||
private void moveAndRenameFiles(List<Resource> resources, PipelineConfig config, Path dir)
|
||||
throws IOException {
|
||||
for (Resource resource : resources) {
|
||||
String outputFileName = createOutputFileName(resource, config);
|
||||
Path outputPath = determineOutputPath(config, dir);
|
||||
@ -217,18 +224,27 @@ public class PipelineDirectoryProcessor {
|
||||
String baseName = resourceName.substring(0, resourceName.lastIndexOf('.'));
|
||||
String extension = resourceName.substring(resourceName.lastIndexOf('.') + 1);
|
||||
|
||||
String outputFileName = config.getOutputPattern()
|
||||
String outputFileName =
|
||||
config.getOutputPattern()
|
||||
.replace("{filename}", baseName)
|
||||
.replace("{pipelineName}", config.getName())
|
||||
.replace("{date}", LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")))
|
||||
.replace("{time}", LocalTime.now().format(DateTimeFormatter.ofPattern("HHmmss")))
|
||||
+ "." + extension;
|
||||
.replace(
|
||||
"{date}",
|
||||
LocalDate.now()
|
||||
.format(DateTimeFormatter.ofPattern("yyyyMMdd")))
|
||||
.replace(
|
||||
"{time}",
|
||||
LocalTime.now()
|
||||
.format(DateTimeFormatter.ofPattern("HHmmss")))
|
||||
+ "."
|
||||
+ extension;
|
||||
|
||||
return outputFileName;
|
||||
}
|
||||
|
||||
private Path determineOutputPath(PipelineConfig config, Path dir) {
|
||||
String outputDir = config.getOutputDir()
|
||||
String outputDir =
|
||||
config.getOutputDir()
|
||||
.replace("{outputFolder}", finishedFoldersDir)
|
||||
.replace("{folderName}", dir.toString())
|
||||
.replaceAll("\\\\?watchedFolders", "");
|
||||
@ -236,7 +252,8 @@ public class PipelineDirectoryProcessor {
|
||||
return Paths.get(outputDir).isAbsolute() ? Paths.get(outputDir) : Paths.get(".", outputDir);
|
||||
}
|
||||
|
||||
private void deleteOriginalFiles(List<File> filesToProcess, Path processingDir) throws IOException {
|
||||
private void deleteOriginalFiles(List<File> filesToProcess, Path processingDir)
|
||||
throws IOException {
|
||||
for (File file : filesToProcess) {
|
||||
Files.deleteIfExists(processingDir.resolve(file.getName()));
|
||||
logger.info("Deleted original file: {}", file.getName());
|
||||
@ -247,12 +264,13 @@ public class PipelineDirectoryProcessor {
|
||||
for (File file : filesToProcess) {
|
||||
try {
|
||||
Files.move(processingDir.resolve(file.getName()), file.toPath());
|
||||
logger.info("Moved file back to original location: {} , {}",file.toPath(), file.getName());
|
||||
logger.info(
|
||||
"Moved file back to original location: {} , {}",
|
||||
file.toPath(),
|
||||
file.getName());
|
||||
} catch (IOException e) {
|
||||
logger.error("Error moving file back to original location: {}", file.getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -5,6 +5,8 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@ -34,7 +36,6 @@ import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.servlet.ServletContext;
|
||||
|
||||
import stirling.software.SPDF.SPdfApplication;
|
||||
import stirling.software.SPDF.model.PipelineConfig;
|
||||
import stirling.software.SPDF.model.PipelineOperation;
|
||||
@ -45,26 +46,18 @@ public class PipelineProcessor {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PipelineProcessor.class);
|
||||
|
||||
|
||||
@Autowired
|
||||
private ApiDocService apiDocService;
|
||||
@Autowired private ApiDocService apiDocService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private UserServiceInterface userService;
|
||||
|
||||
@Autowired
|
||||
private ServletContext servletContext;
|
||||
|
||||
|
||||
|
||||
@Autowired private ServletContext servletContext;
|
||||
|
||||
private String getApiKeyForUser() {
|
||||
if (userService == null)
|
||||
return "";
|
||||
if (userService == null) return "";
|
||||
return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId());
|
||||
}
|
||||
|
||||
|
||||
private String getBaseUrl() {
|
||||
String contextPath = servletContext.getContextPath();
|
||||
String port = SPdfApplication.getPort();
|
||||
@ -72,9 +65,8 @@ public class PipelineProcessor {
|
||||
return "http://localhost:" + port + contextPath + "/";
|
||||
}
|
||||
|
||||
|
||||
|
||||
List<Resource> runPipelineAgainstFiles(List<Resource> outputFiles, PipelineConfig config) throws Exception {
|
||||
List<Resource> runPipelineAgainstFiles(List<Resource> outputFiles, PipelineConfig config)
|
||||
throws Exception {
|
||||
|
||||
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
|
||||
PrintStream logPrintStream = new PrintStream(logStream);
|
||||
@ -85,7 +77,10 @@ public class PipelineProcessor {
|
||||
String operation = pipelineOperation.getOperation();
|
||||
boolean isMultiInputOperation = apiDocService.isMultiInput(operation);
|
||||
|
||||
logger.info("Running operation: {} isMultiInputOperation {}", operation, isMultiInputOperation);
|
||||
logger.info(
|
||||
"Running operation: {} isMultiInputOperation {}",
|
||||
operation,
|
||||
isMultiInputOperation);
|
||||
Map<String, Object> parameters = pipelineOperation.getParameters();
|
||||
String inputFileExtension = "";
|
||||
|
||||
@ -108,14 +103,14 @@ public class PipelineProcessor {
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("fileInput", file);
|
||||
|
||||
|
||||
for (Entry<String, Object> entry : parameters.entrySet()) {
|
||||
body.add(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
||||
|
||||
// If the operation is filter and the response body is null or empty, skip this
|
||||
// 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)) {
|
||||
@ -129,22 +124,26 @@ public class PipelineProcessor {
|
||||
continue;
|
||||
}
|
||||
processOutputFiles(operation, file.getFilename(), response, newOutputFiles);
|
||||
|
||||
}
|
||||
|
||||
if (!hasInputFileType) {
|
||||
logPrintStream.println(
|
||||
"No files with extension " + inputFileExtension + " found for operation " + operation);
|
||||
"No files with extension "
|
||||
+ inputFileExtension
|
||||
+ " found for operation "
|
||||
+ operation);
|
||||
hasErrors = true;
|
||||
}
|
||||
|
||||
outputFiles = newOutputFiles;
|
||||
}
|
||||
|
||||
} else {
|
||||
// Filter and collect all files that match the inputFileExtension
|
||||
List<Resource> matchingFiles = outputFiles.stream()
|
||||
.filter(file -> file.getFilename().endsWith(finalInputFileExtension))
|
||||
List<Resource> matchingFiles =
|
||||
outputFiles.stream()
|
||||
.filter(
|
||||
file ->
|
||||
file.getFilename()
|
||||
.endsWith(finalInputFileExtension))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Check if there are matching files
|
||||
@ -165,23 +164,33 @@ public class PipelineProcessor {
|
||||
|
||||
// Handle the response
|
||||
if (response.getStatusCode().equals(HttpStatus.OK)) {
|
||||
processOutputFiles(operation, matchingFiles.get(0).getFilename(), response, newOutputFiles);
|
||||
processOutputFiles(
|
||||
operation,
|
||||
matchingFiles.get(0).getFilename(),
|
||||
response,
|
||||
newOutputFiles);
|
||||
} else {
|
||||
// Log error if the response status is not OK
|
||||
logPrintStream.println("Error in multi-input operation: " + response.getBody());
|
||||
logPrintStream.println(
|
||||
"Error in multi-input operation: " + response.getBody());
|
||||
hasErrors = true;
|
||||
}
|
||||
} else {
|
||||
logPrintStream.println("No files with extension " + inputFileExtension + " found for multi-input operation " + operation);
|
||||
logPrintStream.println(
|
||||
"No files with extension "
|
||||
+ inputFileExtension
|
||||
+ " found for multi-input operation "
|
||||
+ operation);
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
logPrintStream.close();
|
||||
|
||||
outputFiles = newOutputFiles;
|
||||
}
|
||||
if (hasErrors) {
|
||||
logger.error("Errors occurred during processing. Log: {}", logStream.toString());
|
||||
}
|
||||
|
||||
return outputFiles;
|
||||
}
|
||||
|
||||
@ -189,6 +198,7 @@ public class PipelineProcessor {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
// Set up headers, including API key
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
String apiKey = getApiKeyForUser();
|
||||
headers.add("X-API-Key", apiKey);
|
||||
@ -201,14 +211,20 @@ public class PipelineProcessor {
|
||||
return restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
|
||||
}
|
||||
|
||||
private List<Resource> processOutputFiles(String operation, String fileName, ResponseEntity<byte[]> response, List<Resource> newOutputFiles) throws IOException{
|
||||
private List<Resource> processOutputFiles(
|
||||
String operation,
|
||||
String fileName,
|
||||
ResponseEntity<byte[]> response,
|
||||
List<Resource> newOutputFiles)
|
||||
throws IOException {
|
||||
// Define filename
|
||||
String newFilename;
|
||||
if ("auto-rename".equals(operation)) {
|
||||
if (operation.contains("auto-rename")) {
|
||||
// If the operation is "auto-rename", generate a new filename.
|
||||
// This is a simple example of generating a filename using current timestamp.
|
||||
// Modify as per your needs.
|
||||
newFilename = "file_" + System.currentTimeMillis();
|
||||
|
||||
newFilename = extractFilename(response);
|
||||
} else {
|
||||
// Otherwise, keep the original filename.
|
||||
newFilename = fileName;
|
||||
@ -219,7 +235,8 @@ public class PipelineProcessor {
|
||||
// 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()) {
|
||||
Resource outputResource =
|
||||
new ByteArrayResource(response.getBody()) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return newFilename;
|
||||
@ -229,15 +246,36 @@ public class PipelineProcessor {
|
||||
}
|
||||
|
||||
return newOutputFiles;
|
||||
|
||||
}
|
||||
|
||||
public String extractFilename(ResponseEntity<byte[]> response) {
|
||||
String filename = "default-filename.ext"; // Default filename if not found
|
||||
|
||||
HttpHeaders headers = response.getHeaders();
|
||||
String contentDisposition = headers.getFirst(HttpHeaders.CONTENT_DISPOSITION);
|
||||
|
||||
if (contentDisposition != null && !contentDisposition.isEmpty()) {
|
||||
String[] parts = contentDisposition.split(";");
|
||||
for (String part : parts) {
|
||||
if (part.trim().startsWith("filename")) {
|
||||
// Extracts filename and removes quotes if present
|
||||
filename = part.split("=")[1].trim().replace("\"", "");
|
||||
filename = URLDecoder.decode(filename, StandardCharsets.UTF_8);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
List<Resource> generateInputFiles(File[] files) throws Exception {
|
||||
if (files == null || files.length == 0) {
|
||||
logger.info("No files");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
List<Resource> outputFiles = new ArrayList<>();
|
||||
|
||||
for (File file : files) {
|
||||
@ -245,7 +283,8 @@ public class PipelineProcessor {
|
||||
logger.info("Reading file: " + path); // debug statement
|
||||
|
||||
if (Files.exists(path)) {
|
||||
Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) {
|
||||
Resource fileResource =
|
||||
new ByteArrayResource(Files.readAllBytes(path)) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return file.getName();
|
||||
@ -269,7 +308,8 @@ public class PipelineProcessor {
|
||||
List<Resource> outputFiles = new ArrayList<>();
|
||||
|
||||
for (MultipartFile file : files) {
|
||||
Resource fileResource = new ByteArrayResource(file.getBytes()) {
|
||||
Resource fileResource =
|
||||
new ByteArrayResource(file.getBytes()) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return file.getOriginalFilename();
|
||||
@ -308,7 +348,8 @@ public class PipelineProcessor {
|
||||
}
|
||||
|
||||
final String filename = entry.getName();
|
||||
Resource fileResource = new ByteArrayResource(baos.toByteArray()) {
|
||||
Resource fileResource =
|
||||
new ByteArrayResource(baos.toByteArray()) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
@ -328,5 +369,4 @@ public class PipelineProcessor {
|
||||
logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
|
||||
return unzippedFiles;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
package stirling.software.SPDF.controller.api.pipeline;
|
||||
|
||||
public interface UserServiceInterface {
|
||||
String getApiKeyForUser(String username);
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@ -68,8 +69,12 @@ 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. Input:PDF Output:PDF Type:MF-SISO")
|
||||
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) throws Exception {
|
||||
@Operation(
|
||||
summary = "Sign PDF with a Digital Certificate",
|
||||
description =
|
||||
"This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:MF-SISO")
|
||||
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request)
|
||||
throws Exception {
|
||||
MultipartFile pdf = request.getFileInput();
|
||||
String certType = request.getCertType();
|
||||
MultipartFile privateKeyFile = request.getPrivateKeyFile();
|
||||
@ -91,10 +96,13 @@ public class CertSignController {
|
||||
case "PKCS12":
|
||||
if (p12File != null) {
|
||||
KeyStore ks = KeyStore.getInstance("PKCS12");
|
||||
ks.load(new ByteArrayInputStream(p12File.getBytes()), password.toCharArray());
|
||||
ks.load(
|
||||
new ByteArrayInputStream(p12File.getBytes()),
|
||||
password.toCharArray());
|
||||
String alias = ks.aliases().nextElement();
|
||||
if (!ks.isKeyEntry(alias)) {
|
||||
throw new IllegalArgumentException("The provided PKCS12 file does not contain a private key.");
|
||||
throw new IllegalArgumentException(
|
||||
"The provided PKCS12 file does not contain a private key.");
|
||||
}
|
||||
privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray());
|
||||
cert = (X509Certificate) ks.getCertificate(alias);
|
||||
@ -103,23 +111,34 @@ public class CertSignController {
|
||||
case "PEM":
|
||||
if (privateKeyFile != null && certFile != null) {
|
||||
// Load private key
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME);
|
||||
KeyFactory keyFactory =
|
||||
KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME);
|
||||
if (isPEM(privateKeyFile.getBytes())) {
|
||||
privateKey = keyFactory
|
||||
.generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes())));
|
||||
privateKey =
|
||||
keyFactory.generatePrivate(
|
||||
new PKCS8EncodedKeySpec(
|
||||
parsePEM(privateKeyFile.getBytes())));
|
||||
} else {
|
||||
privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes()));
|
||||
privateKey =
|
||||
keyFactory.generatePrivate(
|
||||
new PKCS8EncodedKeySpec(privateKeyFile.getBytes()));
|
||||
}
|
||||
|
||||
// Load certificate
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509",
|
||||
BouncyCastleProvider.PROVIDER_NAME);
|
||||
CertificateFactory certFactory =
|
||||
CertificateFactory.getInstance(
|
||||
"X.509", BouncyCastleProvider.PROVIDER_NAME);
|
||||
if (isPEM(certFile.getBytes())) {
|
||||
cert = (X509Certificate) certFactory
|
||||
.generateCertificate(new ByteArrayInputStream(parsePEM(certFile.getBytes())));
|
||||
cert =
|
||||
(X509Certificate)
|
||||
certFactory.generateCertificate(
|
||||
new ByteArrayInputStream(
|
||||
parsePEM(certFile.getBytes())));
|
||||
} else {
|
||||
cert = (X509Certificate) certFactory
|
||||
.generateCertificate(new ByteArrayInputStream(certFile.getBytes()));
|
||||
cert =
|
||||
(X509Certificate)
|
||||
certFactory.generateCertificate(
|
||||
new ByteArrayInputStream(certFile.getBytes()));
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -154,7 +173,8 @@ public class CertSignController {
|
||||
|
||||
PDSignatureField signatureField = new PDSignatureField(acroForm);
|
||||
PDAnnotationWidget widget = signatureField.getWidgets().get(0);
|
||||
PDRectangle rect = new PDRectangle(100, 100, 200, 50); // Define the rectangle size here
|
||||
PDRectangle rect =
|
||||
new PDRectangle(100, 100, 200, 50); // Define the rectangle size here
|
||||
widget.setRectangle(rect);
|
||||
page.getAnnotations().add(widget);
|
||||
|
||||
@ -166,13 +186,18 @@ public class CertSignController {
|
||||
appearanceDict.setNormalAppearance(appearanceStream);
|
||||
widget.setAppearance(appearanceDict);
|
||||
|
||||
try (PDPageContentStream contentStream = new PDPageContentStream(document, appearanceStream)) {
|
||||
try (PDPageContentStream contentStream =
|
||||
new PDPageContentStream(document, appearanceStream)) {
|
||||
contentStream.beginText();
|
||||
contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12);
|
||||
contentStream.newLineAtOffset(110, 130);
|
||||
contentStream.showText("Digitally signed by: " + (name != null ? name : "Unknown"));
|
||||
contentStream.showText(
|
||||
"Digitally signed by: " + (name != null ? name : "Unknown"));
|
||||
contentStream.newLineAtOffset(0, -15);
|
||||
contentStream.showText("Date: " + new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z").format(new Date()));
|
||||
contentStream.showText(
|
||||
"Date: "
|
||||
+ new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z")
|
||||
.format(new Date()));
|
||||
contentStream.newLineAtOffset(0, -15);
|
||||
if (reason != null && !reason.isEmpty()) {
|
||||
contentStream.showText("Reason: " + reason);
|
||||
@ -205,8 +230,8 @@ public class CertSignController {
|
||||
document.addSignature(signature, signatureOptions);
|
||||
logger.info("Signature added to the PDF document");
|
||||
// External signing
|
||||
ExternalSigningSupport externalSigning = document
|
||||
.saveIncrementalForExternalSigning(new ByteArrayOutputStream());
|
||||
ExternalSigningSupport externalSigning =
|
||||
document.saveIncrementalForExternalSigning(new ByteArrayOutputStream());
|
||||
|
||||
byte[] content = IOUtils.toByteArray(externalSigning.getContent());
|
||||
|
||||
@ -214,11 +239,16 @@ public class CertSignController {
|
||||
CMSTypedData cmsData = new CMSProcessableByteArray(content);
|
||||
|
||||
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
|
||||
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
|
||||
.setProvider(BouncyCastleProvider.PROVIDER_NAME).build(privateKey);
|
||||
ContentSigner signer =
|
||||
new JcaContentSignerBuilder("SHA256withRSA")
|
||||
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||
.build(privateKey);
|
||||
|
||||
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(
|
||||
new JcaDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build())
|
||||
gen.addSignerInfoGenerator(
|
||||
new JcaSignerInfoGeneratorBuilder(
|
||||
new JcaDigestCalculatorProviderBuilder()
|
||||
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||
.build())
|
||||
.build(signer, cert));
|
||||
|
||||
gen.addCertificates(new JcaCertStore(Collections.singletonList(cert)));
|
||||
@ -232,7 +262,8 @@ public class CertSignController {
|
||||
// After setting the signature, return the resultant PDF
|
||||
try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) {
|
||||
document.save(signedPdfOutput);
|
||||
return WebResponseUtils.boasToWebResponse(signedPdfOutput,
|
||||
return WebResponseUtils.boasToWebResponse(
|
||||
signedPdfOutput,
|
||||
pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_signed.pdf");
|
||||
|
||||
} catch (Exception e) {
|
||||
@ -246,7 +277,8 @@ public class CertSignController {
|
||||
}
|
||||
|
||||
private byte[] parsePEM(byte[] content) throws IOException {
|
||||
PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(content)));
|
||||
PemReader pemReader =
|
||||
new PemReader(new InputStreamReader(new ByteArrayInputStream(content)));
|
||||
return pemReader.readPemObject().getContent();
|
||||
}
|
||||
|
||||
@ -254,5 +286,4 @@ public class CertSignController {
|
||||
String contentStr = new String(content);
|
||||
return contentStr.contains("-----BEGIN") && contentStr.contains("-----END");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -72,8 +72,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.PDFFile;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/security")
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
@ -83,12 +85,9 @@ public class GetInfoOnPDF {
|
||||
|
||||
@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(@ModelAttribute PDFFile request)
|
||||
throws IOException {
|
||||
public ResponseEntity<byte[]> getPdfInfo(@ModelAttribute PDFFile request) throws IOException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
try (
|
||||
PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream());
|
||||
) {
|
||||
try (PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream()); ) {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
ObjectNode jsonOutput = objectMapper.createObjectNode();
|
||||
|
||||
@ -101,7 +100,6 @@ public class GetInfoOnPDF {
|
||||
ObjectNode encryption = objectMapper.createObjectNode();
|
||||
ObjectNode other = objectMapper.createObjectNode();
|
||||
|
||||
|
||||
metadata.put("Title", info.getTitle());
|
||||
metadata.put("Author", info.getAuthor());
|
||||
metadata.put("Subject", info.getSubject());
|
||||
@ -112,9 +110,6 @@ public class GetInfoOnPDF {
|
||||
metadata.put("ModificationDate", formatDate(info.getModificationDate()));
|
||||
jsonOutput.set("Metadata", metadata);
|
||||
|
||||
|
||||
|
||||
|
||||
// Total file size of the PDF
|
||||
long fileSizeInBytes = inputFile.getSize();
|
||||
basicInfo.put("FileSizeInBytes", fileSizeInBytes);
|
||||
@ -130,7 +125,6 @@ public class GetInfoOnPDF {
|
||||
int charCount = fullText.length();
|
||||
basicInfo.put("CharacterCount", charCount);
|
||||
|
||||
|
||||
// Initialize the flags and types
|
||||
boolean hasCompression = false;
|
||||
String compressionType = "None";
|
||||
@ -147,25 +141,20 @@ public class GetInfoOnPDF {
|
||||
}
|
||||
}
|
||||
basicInfo.put("Compression", hasCompression);
|
||||
if(hasCompression)
|
||||
basicInfo.put("CompressionType", compressionType);
|
||||
if (hasCompression) basicInfo.put("CompressionType", compressionType);
|
||||
|
||||
String language = pdfBoxDoc.getDocumentCatalog().getLanguage();
|
||||
basicInfo.put("Language", language);
|
||||
basicInfo.put("Number of pages", pdfBoxDoc.getNumberOfPages());
|
||||
|
||||
|
||||
PDDocumentCatalog catalog = pdfBoxDoc.getDocumentCatalog();
|
||||
String pageMode = catalog.getPageMode().name();
|
||||
|
||||
// Document Information using PDFBox
|
||||
docInfoNode.put("PDF version", pdfBoxDoc.getVersion());
|
||||
docInfoNode.put("Trapped", info.getTrapped());
|
||||
docInfoNode.put("Page Mode", getPageModeDescription(pageMode));;
|
||||
|
||||
|
||||
|
||||
|
||||
docInfoNode.put("Page Mode", getPageModeDescription(pageMode));
|
||||
;
|
||||
|
||||
PDAcroForm acroForm = pdfBoxDoc.getDocumentCatalog().getAcroForm();
|
||||
|
||||
@ -177,11 +166,6 @@ public class GetInfoOnPDF {
|
||||
}
|
||||
jsonOutput.set("FormFields", formFieldsNode);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// embeed files TODO size
|
||||
if (catalog.getNames() != null) {
|
||||
PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles();
|
||||
@ -190,12 +174,14 @@ public class GetInfoOnPDF {
|
||||
if (efTree != null) {
|
||||
Map<String, PDComplexFileSpecification> efMap = efTree.getNames();
|
||||
if (efMap != null) {
|
||||
for (Map.Entry<String, PDComplexFileSpecification> entry : efMap.entrySet()) {
|
||||
for (Map.Entry<String, PDComplexFileSpecification> entry :
|
||||
efMap.entrySet()) {
|
||||
ObjectNode embeddedFileNode = objectMapper.createObjectNode();
|
||||
embeddedFileNode.put("Name", entry.getKey());
|
||||
PDEmbeddedFile embeddedFile = entry.getValue().getEmbeddedFile();
|
||||
if (embeddedFile != null) {
|
||||
embeddedFileNode.put("FileSize", embeddedFile.getLength()); // size in bytes
|
||||
embeddedFileNode.put(
|
||||
"FileSize", embeddedFile.getLength()); // size in bytes
|
||||
}
|
||||
embeddedFilesArray.add(embeddedFileNode);
|
||||
}
|
||||
@ -204,14 +190,13 @@ public class GetInfoOnPDF {
|
||||
other.set("EmbeddedFiles", embeddedFilesArray);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// attachments TODO size
|
||||
ArrayNode attachmentsArray = objectMapper.createArrayNode();
|
||||
for (PDPage page : pdfBoxDoc.getPages()) {
|
||||
for (PDAnnotation annotation : page.getAnnotations()) {
|
||||
if (annotation instanceof PDAnnotationFileAttachment) {
|
||||
PDAnnotationFileAttachment fileAttachmentAnnotation = (PDAnnotationFileAttachment) annotation;
|
||||
PDAnnotationFileAttachment fileAttachmentAnnotation =
|
||||
(PDAnnotationFileAttachment) annotation;
|
||||
|
||||
ObjectNode attachmentNode = objectMapper.createObjectNode();
|
||||
attachmentNode.put("Name", fileAttachmentAnnotation.getAttachmentName());
|
||||
@ -254,9 +239,9 @@ public class GetInfoOnPDF {
|
||||
}
|
||||
other.set("JavaScript", javascriptArray);
|
||||
|
||||
|
||||
// TODO size
|
||||
PDOptionalContentProperties ocProperties = pdfBoxDoc.getDocumentCatalog().getOCProperties();
|
||||
PDOptionalContentProperties ocProperties =
|
||||
pdfBoxDoc.getDocumentCatalog().getOCProperties();
|
||||
ArrayNode layersArray = objectMapper.createArrayNode();
|
||||
|
||||
if (ocProperties != null) {
|
||||
@ -271,11 +256,8 @@ public class GetInfoOnPDF {
|
||||
|
||||
// TODO Security
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
PDStructureTreeRoot structureTreeRoot = pdfBoxDoc.getDocumentCatalog().getStructureTreeRoot();
|
||||
PDStructureTreeRoot structureTreeRoot =
|
||||
pdfBoxDoc.getDocumentCatalog().getStructureTreeRoot();
|
||||
ArrayNode structureTreeArray;
|
||||
try {
|
||||
if (structureTreeRoot != null) {
|
||||
@ -287,14 +269,21 @@ public class GetInfoOnPDF {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A");
|
||||
boolean isPdfXCompliant = checkForStandard(pdfBoxDoc, "PDF/X");
|
||||
boolean isPdfECompliant = checkForStandard(pdfBoxDoc, "PDF/E");
|
||||
boolean isPdfVTCompliant = checkForStandard(pdfBoxDoc, "PDF/VT");
|
||||
boolean isPdfUACompliant = checkForStandard(pdfBoxDoc, "PDF/UA");
|
||||
boolean isPdfBCompliant = checkForStandard(pdfBoxDoc, "PDF/B"); // If you want to check for PDF/Broadcast, though this isn't an official ISO standard.
|
||||
boolean isPdfSECCompliant = checkForStandard(pdfBoxDoc, "PDF/SEC"); // This might not be effective since PDF/SEC was under development in 2021.
|
||||
boolean isPdfBCompliant =
|
||||
checkForStandard(
|
||||
pdfBoxDoc,
|
||||
"PDF/B"); // If you want to check for PDF/Broadcast, though this isn't
|
||||
// an official ISO standard.
|
||||
boolean isPdfSECCompliant =
|
||||
checkForStandard(
|
||||
pdfBoxDoc,
|
||||
"PDF/SEC"); // This might not be effective since PDF/SEC was under
|
||||
// development in 2021.
|
||||
|
||||
compliancy.put("IsPDF/ACompliant", isPdfACompliant);
|
||||
compliancy.put("IsPDF/XCompliant", isPdfXCompliant);
|
||||
@ -304,10 +293,6 @@ public class GetInfoOnPDF {
|
||||
compliancy.put("IsPDF/BCompliant", isPdfBCompliant);
|
||||
compliancy.put("IsPDF/SECCompliant", isPdfSECCompliant);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
PDOutlineNode root = pdfBoxDoc.getDocumentCatalog().getDocumentOutline();
|
||||
ArrayNode bookmarksArray = objectMapper.createArrayNode();
|
||||
|
||||
@ -319,8 +304,6 @@ public class GetInfoOnPDF {
|
||||
|
||||
other.set("Bookmarks/Outline/TOC", bookmarksArray);
|
||||
|
||||
|
||||
|
||||
PDMetadata pdMetadata = pdfBoxDoc.getDocumentCatalog().getMetadata();
|
||||
|
||||
String xmpString = null;
|
||||
@ -341,8 +324,6 @@ public class GetInfoOnPDF {
|
||||
|
||||
other.put("XMPMetadata", xmpString);
|
||||
|
||||
|
||||
|
||||
if (pdfBoxDoc.isEncrypted()) {
|
||||
encryption.put("IsEncrypted", true);
|
||||
|
||||
@ -356,23 +337,22 @@ public class GetInfoOnPDF {
|
||||
|
||||
permissionsNode.put("CanAssembleDocument", ap.canAssembleDocument());
|
||||
permissionsNode.put("CanExtractContent", ap.canExtractContent());
|
||||
permissionsNode.put("CanExtractForAccessibility", ap.canExtractForAccessibility());
|
||||
permissionsNode.put(
|
||||
"CanExtractForAccessibility", ap.canExtractForAccessibility());
|
||||
permissionsNode.put("CanFillInForm", ap.canFillInForm());
|
||||
permissionsNode.put("CanModify", ap.canModify());
|
||||
permissionsNode.put("CanModifyAnnotations", ap.canModifyAnnotations());
|
||||
permissionsNode.put("CanPrint", ap.canPrint());
|
||||
permissionsNode.put("CanPrintDegraded", ap.canPrintDegraded());
|
||||
|
||||
encryption.set("Permissions", permissionsNode); // set the node under "Permissions"
|
||||
encryption.set(
|
||||
"Permissions", permissionsNode); // set the node under "Permissions"
|
||||
}
|
||||
// Add other encryption-related properties as needed
|
||||
} else {
|
||||
encryption.put("IsEncrypted", false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
ObjectNode pageInfoParent = objectMapper.createObjectNode();
|
||||
for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) {
|
||||
ObjectNode pageInfo = objectMapper.createObjectNode();
|
||||
@ -396,11 +376,11 @@ public class GetInfoOnPDF {
|
||||
pageInfo.put("Rotation", page.getRotation());
|
||||
pageInfo.put("Page Orientation", getPageOrientation(width, height));
|
||||
|
||||
|
||||
// Boxes
|
||||
pageInfo.put("MediaBox", mediaBox.toString());
|
||||
|
||||
// Assuming the following boxes are defined for your document; if not, you may get null values.
|
||||
// Assuming the following boxes are defined for your document; if not, you may get
|
||||
// null values.
|
||||
PDRectangle cropBox = page.getCropBox();
|
||||
pageInfo.put("CropBox", cropBox == null ? "Undefined" : cropBox.toString());
|
||||
|
||||
@ -443,15 +423,12 @@ public class GetInfoOnPDF {
|
||||
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();
|
||||
PDResources resources = page.getResources();
|
||||
|
||||
|
||||
for (COSName name : resources.getXObjectNames()) {
|
||||
PDXObject xObject = resources.getXObject(name);
|
||||
if (xObject instanceof PDImageXObject) {
|
||||
@ -460,7 +437,9 @@ public class GetInfoOnPDF {
|
||||
ObjectNode imageNode = objectMapper.createObjectNode();
|
||||
imageNode.put("Width", image.getWidth());
|
||||
imageNode.put("Height", image.getHeight());
|
||||
if(image.getMetadata() != null && image.getMetadata().getFile() != null && image.getMetadata().getFile().getFile() != null) {
|
||||
if (image.getMetadata() != null
|
||||
&& image.getMetadata().getFile() != null
|
||||
&& image.getMetadata().getFile().getFile() != null) {
|
||||
imageNode.put("Name", image.getMetadata().getFile().getFile());
|
||||
}
|
||||
if (image.getColorSpace() != null) {
|
||||
@ -472,7 +451,6 @@ public class GetInfoOnPDF {
|
||||
}
|
||||
pageInfo.set("Images", imagesArray);
|
||||
|
||||
|
||||
// Links
|
||||
ArrayNode linksArray = objectMapper.createArrayNode();
|
||||
Set<String> uniqueURIs = new HashSet<>(); // To store unique URIs
|
||||
@ -496,7 +474,6 @@ public class GetInfoOnPDF {
|
||||
}
|
||||
pageInfo.set("Links", linksArray);
|
||||
|
||||
|
||||
// Fonts
|
||||
ArrayNode fontsArray = objectMapper.createArrayNode();
|
||||
Map<String, ObjectNode> uniqueFontsMap = new HashMap<>();
|
||||
@ -526,11 +503,11 @@ public class GetInfoOnPDF {
|
||||
fontNode.put("IsNonsymbolic", (flags & 32) != 0);
|
||||
|
||||
fontNode.put("FontFamily", fontDescriptor.getFontFamily());
|
||||
// Font stretch and BBox are not directly available in PDFBox's API, so these are omitted for simplicity
|
||||
// Font stretch and BBox are not directly available in PDFBox's API, so
|
||||
// these are omitted for simplicity
|
||||
fontNode.put("FontWeight", fontDescriptor.getFontWeight());
|
||||
}
|
||||
|
||||
|
||||
// Create a unique key for this font node based on its attributes
|
||||
String uniqueKey = fontNode.toString();
|
||||
|
||||
@ -552,16 +529,6 @@ public class GetInfoOnPDF {
|
||||
|
||||
pageInfo.set("Fonts", fontsArray);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Access resources dictionary
|
||||
ArrayNode colorSpacesArray = objectMapper.createArrayNode();
|
||||
|
||||
@ -581,9 +548,9 @@ public class GetInfoOnPDF {
|
||||
}
|
||||
pageInfo.set("Color Spaces & ICC Profiles", colorSpacesArray);
|
||||
|
||||
|
||||
// Other XObjects
|
||||
Map<String, Integer> xObjectCountMap = new HashMap<>(); // To store the count for each type
|
||||
Map<String, Integer> xObjectCountMap =
|
||||
new HashMap<>(); // To store the count for each type
|
||||
for (COSName name : resources.getXObjectNames()) {
|
||||
PDXObject xObject = resources.getXObject(name);
|
||||
String xObjectType;
|
||||
@ -597,7 +564,8 @@ public class GetInfoOnPDF {
|
||||
}
|
||||
|
||||
// Increment the count for this type in the map
|
||||
xObjectCountMap.put(xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1);
|
||||
xObjectCountMap.put(
|
||||
xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1);
|
||||
}
|
||||
|
||||
// Add the count map to pageInfo (or wherever you want to store it)
|
||||
@ -607,9 +575,6 @@ public class GetInfoOnPDF {
|
||||
}
|
||||
pageInfo.set("XObjectCounts", xObjectCountNode);
|
||||
|
||||
|
||||
|
||||
|
||||
ArrayNode multimediaArray = objectMapper.createArrayNode();
|
||||
|
||||
for (PDAnnotation annotation : annotations) {
|
||||
@ -622,12 +587,9 @@ public class GetInfoOnPDF {
|
||||
|
||||
pageInfo.set("Multimedia", multimediaArray);
|
||||
|
||||
|
||||
|
||||
pageInfoParent.set("Page " + (pageNum + 1), pageInfo);
|
||||
}
|
||||
|
||||
|
||||
jsonOutput.set("BasicInfo", basicInfo);
|
||||
jsonOutput.set("DocumentInfo", docInfoNode);
|
||||
jsonOutput.set("Compliancy", compliancy);
|
||||
@ -635,14 +597,14 @@ public class GetInfoOnPDF {
|
||||
jsonOutput.set("Other", other);
|
||||
jsonOutput.set("PerPageInfo", pageInfoParent);
|
||||
|
||||
|
||||
|
||||
// Save JSON to file
|
||||
String jsonString = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonOutput);
|
||||
String jsonString =
|
||||
objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonOutput);
|
||||
|
||||
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(jsonString.getBytes(StandardCharsets.UTF_8), "response.json", MediaType.APPLICATION_JSON);
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
jsonString.getBytes(StandardCharsets.UTF_8),
|
||||
"response.json",
|
||||
MediaType.APPLICATION_JSON);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
@ -674,6 +636,7 @@ public class GetInfoOnPDF {
|
||||
return "Square";
|
||||
}
|
||||
}
|
||||
|
||||
public String getPageSize(float width, float height) {
|
||||
// Define standard page sizes
|
||||
Map<String, PDRectangle> standardSizes = new HashMap<>();
|
||||
@ -696,12 +659,13 @@ public class GetInfoOnPDF {
|
||||
return "Custom";
|
||||
}
|
||||
|
||||
private boolean isCloseToSize(float width, float height, float standardWidth, float standardHeight) {
|
||||
private boolean isCloseToSize(
|
||||
float width, float height, float standardWidth, float standardHeight) {
|
||||
float tolerance = 1.0f; // You can adjust the tolerance as needed
|
||||
return Math.abs(width - standardWidth) <= tolerance && Math.abs(height - standardHeight) <= tolerance;
|
||||
return Math.abs(width - standardWidth) <= tolerance
|
||||
&& Math.abs(height - standardHeight) <= tolerance;
|
||||
}
|
||||
|
||||
|
||||
public ObjectNode getDimensionInfo(ObjectNode dimensionInfo, float width, float height) {
|
||||
float ppi = 72; // Points Per Inch
|
||||
|
||||
@ -720,8 +684,6 @@ public class GetInfoOnPDF {
|
||||
return dimensionInfo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static boolean checkForStandard(PDDocument document, String standardKeyword) {
|
||||
// Check XMP Metadata
|
||||
try {
|
||||
@ -739,14 +701,16 @@ public static boolean checkForStandard(PDDocument document, String standardKeywo
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) { // Catching general exception for brevity, ideally you'd catch specific exceptions.
|
||||
} catch (
|
||||
Exception
|
||||
e) { // Catching general exception for brevity, ideally you'd catch specific
|
||||
// exceptions.
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public ArrayNode exploreStructureTree(List<Object> nodes) {
|
||||
ArrayNode elementsArray = objectMapper.createArrayNode();
|
||||
if (nodes != null) {
|
||||
@ -773,7 +737,6 @@ public static boolean checkForStandard(PDDocument document, String standardKeywo
|
||||
return elementsArray;
|
||||
}
|
||||
|
||||
|
||||
public String getContent(PDStructureElement structureElement) {
|
||||
StringBuilder contentBuilder = new StringBuilder();
|
||||
|
||||
@ -791,7 +754,6 @@ public static boolean checkForStandard(PDDocument document, String standardKeywo
|
||||
return contentBuilder.toString();
|
||||
}
|
||||
|
||||
|
||||
private String formatDate(Calendar calendar) {
|
||||
if (calendar != null) {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
|
@ -16,9 +16,11 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.security.AddPasswordRequest;
|
||||
import stirling.software.SPDF.model.api.security.PDFPasswordRequest;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/security")
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
@ -26,29 +28,31 @@ public class PasswordController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PasswordController.class);
|
||||
|
||||
|
||||
@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. Input:PDF Output:PDF Type:SISO"
|
||||
)
|
||||
public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request) throws IOException {
|
||||
description =
|
||||
"This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request)
|
||||
throws IOException {
|
||||
MultipartFile fileInput = request.getFileInput();
|
||||
String password = request.getPassword();
|
||||
|
||||
|
||||
|
||||
PDDocument document = PDDocument.load(fileInput.getBytes(), password);
|
||||
document.setAllSecurityToBeRemoved(true);
|
||||
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_password_removed.pdf");
|
||||
return WebResponseUtils.pdfDocToWebResponse(
|
||||
document,
|
||||
fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||
+ "_password_removed.pdf");
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/add-password")
|
||||
@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. Input:PDF Output:PDF"
|
||||
)
|
||||
public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request) throws IOException {
|
||||
description =
|
||||
"This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF")
|
||||
public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request)
|
||||
throws IOException {
|
||||
MultipartFile fileInput = request.getFileInput();
|
||||
String ownerPassword = request.getOwnerPassword();
|
||||
String password = request.getPassword();
|
||||
@ -81,9 +85,12 @@ public class PasswordController {
|
||||
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");
|
||||
return WebResponseUtils.pdfDocToWebResponse(
|
||||
document,
|
||||
fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||
+ "_permissions.pdf");
|
||||
return WebResponseUtils.pdfDocToWebResponse(
|
||||
document,
|
||||
fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -26,10 +26,12 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.PDFText;
|
||||
import stirling.software.SPDF.model.api.security.RedactPdfRequest;
|
||||
import stirling.software.SPDF.pdf.TextFinder;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/security")
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
@ -37,11 +39,13 @@ 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(@ModelAttribute RedactPdfRequest request) throws Exception {
|
||||
@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(@ModelAttribute RedactPdfRequest request)
|
||||
throws Exception {
|
||||
MultipartFile file = request.getFileInput();
|
||||
String listOfTextString = request.getListOfText();
|
||||
boolean useRegex = request.isUseRegex();
|
||||
@ -66,8 +70,6 @@ public class RedactController {
|
||||
redactColor = Color.BLACK;
|
||||
}
|
||||
|
||||
|
||||
|
||||
for (String text : listOfText) {
|
||||
text = text.trim();
|
||||
System.out.println(text);
|
||||
@ -76,8 +78,6 @@ public class RedactController {
|
||||
redactFoundText(document, foundTexts, customPadding, redactColor);
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (convertPDFToImage) {
|
||||
PDDocument imageDocument = new PDDocument();
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
@ -99,25 +99,31 @@ public class RedactController {
|
||||
document.close();
|
||||
|
||||
byte[] pdfContent = baos.toByteArray();
|
||||
return WebResponseUtils.bytesToWebResponse(pdfContent,
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
pdfContent,
|
||||
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_redacted.pdf");
|
||||
}
|
||||
|
||||
|
||||
private void redactFoundText(PDDocument document, List<PDFText> blocks, float customPadding, Color redactColor) throws IOException {
|
||||
private void redactFoundText(
|
||||
PDDocument document, List<PDFText> blocks, float customPadding, Color redactColor)
|
||||
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);
|
||||
PDPageContentStream contentStream =
|
||||
new PDPageContentStream(
|
||||
document, page, PDPageContentStream.AppendMode.APPEND, true, true);
|
||||
contentStream.setNonStrokingColor(redactColor);
|
||||
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.addRect(
|
||||
block.getX1(),
|
||||
pageBox.getHeight() - block.getY1() - padding,
|
||||
block.getX2() - block.getX1(),
|
||||
block.getY2() - block.getY1() + 2 * padding);
|
||||
contentStream.fill();
|
||||
contentStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
package stirling.software.SPDF.controller.api.security;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.pdfbox.cos.COSDictionary;
|
||||
@ -28,6 +29,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.security.SanitizePdfRequest;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@ -37,9 +39,12 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
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(@ModelAttribute SanitizePdfRequest request) throws IOException {
|
||||
@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(@ModelAttribute SanitizePdfRequest request)
|
||||
throws IOException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
boolean removeJavaScript = request.isRemoveJavaScript();
|
||||
boolean removeEmbeddedFiles = request.isRemoveEmbeddedFiles();
|
||||
@ -68,19 +73,25 @@ public class SanitizeController {
|
||||
sanitizeFonts(document);
|
||||
}
|
||||
|
||||
return WebResponseUtils.pdfDocToWebResponse(document, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_sanitized.pdf");
|
||||
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);
|
||||
COSDictionary namesDict =
|
||||
(COSDictionary) catalog.getCOSObject().getDictionaryObject(COSName.NAMES);
|
||||
|
||||
if (namesDict != null) {
|
||||
// Get the JavaScript dictionary
|
||||
COSDictionary javaScriptDict = (COSDictionary) namesDict.getDictionaryObject(COSName.getPDFName("JavaScript"));
|
||||
COSDictionary javaScriptDict =
|
||||
(COSDictionary) namesDict.getDictionaryObject(COSName.getPDFName("JavaScript"));
|
||||
|
||||
if (javaScriptDict != null) {
|
||||
// Remove the JavaScript dictionary
|
||||
@ -121,9 +132,6 @@ public class SanitizeController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private void sanitizeEmbeddedFiles(PDDocument document) {
|
||||
PDPageTree allPages = document.getPages();
|
||||
|
||||
@ -135,7 +143,6 @@ public class SanitizeController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void sanitizeMetadata(PDDocument document) {
|
||||
PDMetadata metadata = document.getDocumentCatalog().getMetadata();
|
||||
if (metadata != null) {
|
||||
@ -143,8 +150,6 @@ public class SanitizeController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void sanitizeLinks(PDDocument document) throws IOException {
|
||||
for (PDPage page : document.getPages()) {
|
||||
for (PDAnnotation annotation : page.getAnnotations()) {
|
||||
@ -163,5 +168,4 @@ public class SanitizeController {
|
||||
page.getResources().getCOSObject().removeItem(COSName.getPDFName("Font"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@ -39,8 +40,12 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
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 type (text or image), rotation, opacity, width spacer, and height spacer. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> addWatermark(@ModelAttribute AddWatermarkRequest request) throws IOException, Exception {
|
||||
@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(@ModelAttribute AddWatermarkRequest request)
|
||||
throws IOException, Exception {
|
||||
MultipartFile pdfFile = request.getFileInput();
|
||||
String watermarkType = request.getWatermarkType();
|
||||
String watermarkText = request.getWatermarkText();
|
||||
@ -59,8 +64,9 @@ public class WatermarkController {
|
||||
for (PDPage page : document.getPages()) {
|
||||
|
||||
// Get the page's content stream
|
||||
PDPageContentStream contentStream = new PDPageContentStream(document, page,
|
||||
PDPageContentStream.AppendMode.APPEND, true);
|
||||
PDPageContentStream contentStream =
|
||||
new PDPageContentStream(
|
||||
document, page, PDPageContentStream.AppendMode.APPEND, true);
|
||||
|
||||
// Set transparency
|
||||
PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
|
||||
@ -68,10 +74,25 @@ public class WatermarkController {
|
||||
contentStream.setGraphicsStateParameters(graphicsState);
|
||||
|
||||
if (watermarkType.equalsIgnoreCase("text")) {
|
||||
addTextWatermark(contentStream, watermarkText, document, page, rotation, widthSpacer, heightSpacer,
|
||||
fontSize, alphabet);
|
||||
addTextWatermark(
|
||||
contentStream,
|
||||
watermarkText,
|
||||
document,
|
||||
page,
|
||||
rotation,
|
||||
widthSpacer,
|
||||
heightSpacer,
|
||||
fontSize,
|
||||
alphabet);
|
||||
} else if (watermarkType.equalsIgnoreCase("image")) {
|
||||
addImageWatermark(contentStream, watermarkImage, document, page, rotation, widthSpacer, heightSpacer,
|
||||
addImageWatermark(
|
||||
contentStream,
|
||||
watermarkImage,
|
||||
document,
|
||||
page,
|
||||
rotation,
|
||||
widthSpacer,
|
||||
heightSpacer,
|
||||
fontSize);
|
||||
}
|
||||
|
||||
@ -79,12 +100,22 @@ public class WatermarkController {
|
||||
contentStream.close();
|
||||
}
|
||||
|
||||
return WebResponseUtils.pdfDocToWebResponse(document,
|
||||
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 {
|
||||
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) {
|
||||
@ -106,12 +137,12 @@ public class WatermarkController {
|
||||
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)) {
|
||||
try (InputStream is = classPathResource.getInputStream();
|
||||
FileOutputStream os = new FileOutputStream(tempFile)) {
|
||||
IOUtils.copy(is, os);
|
||||
}
|
||||
|
||||
@ -134,16 +165,27 @@ public class WatermarkController {
|
||||
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.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 {
|
||||
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());
|
||||
@ -163,8 +205,10 @@ 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));
|
||||
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++) {
|
||||
@ -175,17 +219,18 @@ float y = i * (desiredPhysicalHeight + heightSpacer);
|
||||
contentStream.saveGraphicsState();
|
||||
|
||||
// Create rotation matrix and rotate
|
||||
contentStream.transform(Matrix.getTranslateInstance(x + desiredPhysicalWidth / 2, y + desiredPhysicalHeight / 2));
|
||||
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));
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -24,83 +24,71 @@ import org.apache.pdfbox.text.PDFTextStripperByArea;
|
||||
import org.apache.pdfbox.text.TextPosition;
|
||||
|
||||
/**
|
||||
* Class to extract tabular data from a PDF. Works by making a first pass of the page to group all
|
||||
* nearby text items together, and then inferring a 2D grid from these regions. Each table cell is
|
||||
* then extracted using a PDFTextStripperByArea object.
|
||||
*
|
||||
* Class to extract tabular data from a PDF.
|
||||
* Works by making a first pass of the page to group all nearby text items
|
||||
* together, and then inferring a 2D grid from these regions. Each table cell
|
||||
* is then extracted using a PDFTextStripperByArea object.
|
||||
* <p>Works best when headers are included in the detected region, to ensure representative text in
|
||||
* every column.
|
||||
*
|
||||
* Works best when
|
||||
* headers are included in the detected region, to ensure representative text
|
||||
* in every column.
|
||||
*
|
||||
* Based upon DrawPrintTextLocations PDFBox example
|
||||
* <p>Based upon DrawPrintTextLocations PDFBox example
|
||||
* (https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/org/apache/pdfbox/examples/util/DrawPrintTextLocations.java)
|
||||
*
|
||||
* @author Beldaz
|
||||
*/
|
||||
public class PDFTableStripper extends PDFTextStripper
|
||||
{
|
||||
public class PDFTableStripper extends PDFTextStripper {
|
||||
|
||||
/**
|
||||
* This will print the documents data, for each table cell.
|
||||
*
|
||||
* @param args The command line arguments.
|
||||
*
|
||||
* @throws IOException If there is an error parsing the document.
|
||||
*/
|
||||
/*
|
||||
* Used in methods derived from DrawPrintTextLocations
|
||||
*/
|
||||
private AffineTransform flipAT;
|
||||
|
||||
private AffineTransform rotateAT;
|
||||
|
||||
/**
|
||||
* Regions updated by calls to writeString
|
||||
*/
|
||||
/** Regions updated by calls to writeString */
|
||||
private Set<Rectangle2D> boxes;
|
||||
|
||||
// Border to allow when finding intersections
|
||||
private double dx = 1.0; // This value works for me, feel free to tweak (or add setter)
|
||||
private double dy = 0.000; // Rows of text tend to overlap, so need to extend
|
||||
|
||||
/**
|
||||
* Region in which to find table (otherwise whole page)
|
||||
*/
|
||||
/** Region in which to find table (otherwise whole page) */
|
||||
private Rectangle2D regionArea;
|
||||
|
||||
/**
|
||||
* Number of rows in inferred table
|
||||
*/
|
||||
/** Number of rows in inferred table */
|
||||
private int nRows = 0;
|
||||
|
||||
/**
|
||||
* Number of columns in inferred table
|
||||
*/
|
||||
/** Number of columns in inferred table */
|
||||
private int nCols = 0;
|
||||
|
||||
/**
|
||||
* This is the object that does the text extraction
|
||||
*/
|
||||
/** This is the object that does the text extraction */
|
||||
private PDFTextStripperByArea regionStripper;
|
||||
|
||||
/**
|
||||
* 1D intervals - used for calculateTableRegions()
|
||||
* @author Beldaz
|
||||
*
|
||||
* @author Beldaz
|
||||
*/
|
||||
public static class Interval {
|
||||
double start;
|
||||
double end;
|
||||
|
||||
public Interval(double start, double end) {
|
||||
this.start=start; this.end = end;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
public void add(Interval col) {
|
||||
if(col.start<start)
|
||||
start = col.start;
|
||||
if(col.end>end)
|
||||
end = col.end;
|
||||
if (col.start < start) start = col.start;
|
||||
if (col.end > end) end = col.end;
|
||||
}
|
||||
|
||||
public static void addTo(Interval x, LinkedList<Interval> columns) {
|
||||
int p = 0;
|
||||
Iterator<Interval> it = columns.iterator();
|
||||
@ -118,25 +106,21 @@ public class PDFTableStripper extends PDFTextStripper
|
||||
}
|
||||
while (it.hasNext()) {
|
||||
Interval col = it.next();
|
||||
if(x.start>col.end)
|
||||
break;
|
||||
if (x.start > col.end) break;
|
||||
x.add(col);
|
||||
it.remove();
|
||||
}
|
||||
columns.add(p, x);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Instantiate a new PDFTableStripper object.
|
||||
*
|
||||
* @param document
|
||||
* @throws IOException If there is an error loading the properties.
|
||||
*/
|
||||
public PDFTableStripper() throws IOException
|
||||
{
|
||||
public PDFTableStripper() throws IOException {
|
||||
super.setShouldSeparateByBeads(false);
|
||||
regionStripper = new PDFTextStripperByArea();
|
||||
regionStripper.setSortByPosition(true);
|
||||
@ -147,18 +131,15 @@ public class PDFTableStripper extends PDFTextStripper
|
||||
*
|
||||
* @param rect The rectangle area to retrieve the text from.
|
||||
*/
|
||||
public void setRegion(Rectangle2D rect )
|
||||
{
|
||||
public void setRegion(Rectangle2D rect) {
|
||||
regionArea = rect;
|
||||
}
|
||||
|
||||
public int getRows()
|
||||
{
|
||||
public int getRows() {
|
||||
return nRows;
|
||||
}
|
||||
|
||||
public int getColumns()
|
||||
{
|
||||
public int getColumns() {
|
||||
return nCols;
|
||||
}
|
||||
|
||||
@ -167,13 +148,11 @@ public class PDFTableStripper extends PDFTextStripper
|
||||
*
|
||||
* @return The text that was identified in that region.
|
||||
*/
|
||||
public String getText(int row, int col)
|
||||
{
|
||||
public String getText(int row, int col) {
|
||||
return regionStripper.getTextForRegion("el" + col + "x" + row);
|
||||
}
|
||||
|
||||
public void extractTable(PDPage pdPage) throws IOException
|
||||
{
|
||||
public void extractTable(PDPage pdPage) throws IOException {
|
||||
setStartPage(getCurrentPageNo());
|
||||
setEndPage(getCurrentPageNo());
|
||||
|
||||
@ -186,11 +165,9 @@ public class PDFTableStripper extends PDFTextStripper
|
||||
// page may be rotated
|
||||
rotateAT = new AffineTransform();
|
||||
int rotation = pdPage.getRotation();
|
||||
if (rotation != 0)
|
||||
{
|
||||
if (rotation != 0) {
|
||||
PDRectangle mediaBox = pdPage.getMediaBox();
|
||||
switch (rotation)
|
||||
{
|
||||
switch (rotation) {
|
||||
case 90:
|
||||
rotateAT.translate(mediaBox.getHeight(), 0);
|
||||
break;
|
||||
@ -213,7 +190,8 @@ public class PDFTableStripper extends PDFTextStripper
|
||||
|
||||
Rectangle2D[][] regions = calculateTableRegions();
|
||||
|
||||
// System.err.println("Drawing " + nCols + "x" + nRows + "="+ nRows*nCols + " regions");
|
||||
// System.err.println("Drawing " + nCols + "x" + nRows + "="+ nRows*nCols + "
|
||||
// regions");
|
||||
for (int i = 0; i < nCols; ++i) {
|
||||
for (int j = 0; j < nRows; ++j) {
|
||||
final Rectangle2D region = regions[i][j];
|
||||
@ -227,8 +205,8 @@ public class PDFTableStripper extends PDFTextStripper
|
||||
/**
|
||||
* Infer a rectangular grid of regions from the boxes field.
|
||||
*
|
||||
* @return 2D array of table regions (as Rectangle2D objects). Note that
|
||||
* some of these regions may have no content.
|
||||
* @return 2D array of table regions (as Rectangle2D objects). Note that some of these regions
|
||||
* may have no content.
|
||||
*/
|
||||
private Rectangle2D[][] calculateTableRegions() {
|
||||
|
||||
@ -254,7 +232,12 @@ public class PDFTableStripper extends PDFTextStripper
|
||||
for (Interval column : columns) {
|
||||
int j = 0;
|
||||
for (Interval row : rows) {
|
||||
regions[nCols-i-1][nRows-j-1] = new Rectangle2D.Double(column.start, row.start, column.end - column.start, row.end - row.start);
|
||||
regions[nCols - i - 1][nRows - j - 1] =
|
||||
new Rectangle2D.Double(
|
||||
column.start,
|
||||
row.start,
|
||||
column.end - column.start,
|
||||
row.end - row.start);
|
||||
++j;
|
||||
}
|
||||
++i;
|
||||
@ -264,18 +247,15 @@ public class PDFTableStripper extends PDFTextStripper
|
||||
}
|
||||
|
||||
/**
|
||||
* Register each character's bounding box, updating boxes field to maintain
|
||||
* a list of all distinct groups of characters.
|
||||
* Register each character's bounding box, updating boxes field to maintain a list of all
|
||||
* distinct groups of characters.
|
||||
*
|
||||
* Overrides the default functionality of PDFTextStripper.
|
||||
* Most of this is taken from DrawPrintTextLocations.java, with extra steps
|
||||
* at end of main loop
|
||||
* <p>Overrides the default functionality of PDFTextStripper. Most of this is taken from
|
||||
* DrawPrintTextLocations.java, with extra steps at end of main loop
|
||||
*/
|
||||
@Override
|
||||
protected void writeString(String string, List<TextPosition> textPositions) throws IOException
|
||||
{
|
||||
for (TextPosition text : textPositions)
|
||||
{
|
||||
protected void writeString(String string, List<TextPosition> textPositions) throws IOException {
|
||||
for (TextPosition text : textPositions) {
|
||||
// glyph space -> user space
|
||||
// note: text.getTextMatrix() is *not* the Text Matrix, it's the Text Rendering Matrix
|
||||
AffineTransform at = text.getTextMatrix().createAffineTransform();
|
||||
@ -283,16 +263,15 @@ public class PDFTableStripper extends PDFTextStripper
|
||||
BoundingBox bbox = font.getBoundingBox();
|
||||
|
||||
// advance width, bbox height (glyph space)
|
||||
float xadvance = font.getWidth(text.getCharacterCodes()[0]); // todo: should iterate all chars
|
||||
Rectangle2D.Float rect = new Rectangle2D.Float(0, bbox.getLowerLeftY(), xadvance, bbox.getHeight());
|
||||
float xadvance =
|
||||
font.getWidth(text.getCharacterCodes()[0]); // todo: should iterate all chars
|
||||
Rectangle2D.Float rect =
|
||||
new Rectangle2D.Float(0, bbox.getLowerLeftY(), xadvance, bbox.getHeight());
|
||||
|
||||
if (font instanceof PDType3Font)
|
||||
{
|
||||
if (font instanceof PDType3Font) {
|
||||
// bbox and font matrix are unscaled
|
||||
at.concatenate(font.getFontMatrix().createAffineTransform());
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
// bbox and font matrix are already scaled to 1000
|
||||
at.scale(1 / 1000f, 1 / 1000f);
|
||||
}
|
||||
@ -300,7 +279,6 @@ public class PDFTableStripper extends PDFTextStripper
|
||||
s = flipAT.createTransformedShape(s);
|
||||
s = rotateAT.createTransformedShape(s);
|
||||
|
||||
|
||||
//
|
||||
// Merge character's bounding box with boxes field
|
||||
//
|
||||
@ -326,29 +304,21 @@ public class PDFTableStripper extends PDFTextStripper
|
||||
boxes.remove(box);
|
||||
}
|
||||
boxes.add(bounds);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method does nothing in this derived class, because beads and regions are incompatible. Beads are
|
||||
* ignored when stripping by area.
|
||||
* This method does nothing in this derived class, because beads and regions are incompatible.
|
||||
* Beads are ignored when stripping by area.
|
||||
*
|
||||
* @param aShouldSeparateByBeads The new grouping of beads.
|
||||
*/
|
||||
@Override
|
||||
public final void setShouldSeparateByBeads(boolean aShouldSeparateByBeads)
|
||||
{
|
||||
}
|
||||
public final void setShouldSeparateByBeads(boolean aShouldSeparateByBeads) {}
|
||||
|
||||
/**
|
||||
* Adapted from PDFTextStripperByArea
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
/** Adapted from PDFTextStripperByArea {@inheritDoc} */
|
||||
@Override
|
||||
protected void processTextPosition( TextPosition text )
|
||||
{
|
||||
protected void processTextPosition(TextPosition text) {
|
||||
if (regionArea != null && !regionArea.contains(text.getX(), text.getY())) {
|
||||
// skip character
|
||||
} else {
|
||||
|
@ -1,4 +1,5 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@ -15,16 +16,17 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import stirling.software.SPDF.model.Authority;
|
||||
import stirling.software.SPDF.model.Role;
|
||||
import stirling.software.SPDF.model.User;
|
||||
import stirling.software.SPDF.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()) {
|
||||
@ -42,10 +44,10 @@ public class AccountWebController {
|
||||
|
||||
return "login";
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository; // Assuming you have a repository for user operations
|
||||
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@GetMapping("/addUsers")
|
||||
public String showAddUserForm(Model model, Authentication authentication) {
|
||||
@ -69,7 +71,6 @@ public class AccountWebController {
|
||||
return "addUsers";
|
||||
}
|
||||
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@GetMapping("/account")
|
||||
public String account(HttpServletRequest request, Model model, Authentication authentication) {
|
||||
@ -87,7 +88,9 @@ public class AccountWebController {
|
||||
String username = userDetails.getUsername();
|
||||
|
||||
// Fetch user details from the database
|
||||
Optional<User> user = userRepository.findByUsername(username); // Assuming findByUsername method exists
|
||||
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
|
||||
@ -116,10 +119,10 @@ public class AccountWebController {
|
||||
return "account";
|
||||
}
|
||||
|
||||
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@GetMapping("/change-creds")
|
||||
public String changeCreds(HttpServletRequest request, Model model, Authentication authentication) {
|
||||
public String changeCreds(
|
||||
HttpServletRequest request, Model model, Authentication authentication) {
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
return "redirect:/";
|
||||
}
|
||||
@ -134,7 +137,9 @@ public class AccountWebController {
|
||||
String username = userDetails.getUsername();
|
||||
|
||||
// Fetch user details from the database
|
||||
Optional<User> user = userRepository.findByUsername(username); // Assuming findByUsername method exists
|
||||
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
|
||||
@ -147,6 +152,4 @@ public class AccountWebController {
|
||||
}
|
||||
return "change-creds";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ public class ConverterWebController {
|
||||
model.addAttribute("currentPage", "html-to-pdf");
|
||||
return "convert/html-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/markdown-to-pdf")
|
||||
@Hidden
|
||||
public String convertMarkdownToPdfForm(Model model) {
|
||||
@ -32,7 +33,6 @@ public class ConverterWebController {
|
||||
return "convert/markdown-to-pdf";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/url-to-pdf")
|
||||
@Hidden
|
||||
public String convertURLToPdfForm(Model model) {
|
||||
@ -40,7 +40,6 @@ public class ConverterWebController {
|
||||
return "convert/url-to-pdf";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/pdf-to-img")
|
||||
@Hidden
|
||||
public String pdfToimgForm(Model model) {
|
||||
@ -55,8 +54,6 @@ public class ConverterWebController {
|
||||
return "convert/file-to-pdf";
|
||||
}
|
||||
|
||||
|
||||
|
||||
// PDF TO......
|
||||
|
||||
@GetMapping("/pdf-to-html")
|
||||
@ -107,7 +104,6 @@ public class ConverterWebController {
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/pdf-to-pdfa")
|
||||
@Hidden
|
||||
public String pdfToPdfAForm(Model model) {
|
||||
|
@ -33,9 +33,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class GeneralWebController {
|
||||
|
||||
|
||||
|
||||
|
||||
@GetMapping("/pipeline")
|
||||
@Hidden
|
||||
public String pipelineForm(Model model) {
|
||||
@ -46,8 +43,8 @@ public class GeneralWebController {
|
||||
|
||||
if (new File("./pipeline/defaultWebUIConfigs/").exists()) {
|
||||
try (Stream<Path> paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) {
|
||||
List<Path> jsonFiles = paths
|
||||
.filter(Files::isRegularFile)
|
||||
List<Path> jsonFiles =
|
||||
paths.filter(Files::isRegularFile)
|
||||
.filter(p -> p.toString().endsWith(".json"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
@ -57,18 +54,25 @@ public class GeneralWebController {
|
||||
}
|
||||
|
||||
for (String config : pipelineConfigs) {
|
||||
Map<String, Object> jsonContent = new ObjectMapper().readValue(config, new TypeReference<Map<String, Object>>(){});
|
||||
Map<String, Object> jsonContent =
|
||||
new ObjectMapper()
|
||||
.readValue(config, new TypeReference<Map<String, Object>>() {});
|
||||
|
||||
String name = (String) jsonContent.get("name");
|
||||
if (name == null || name.length() < 1) {
|
||||
String filename =
|
||||
jsonFiles
|
||||
.get(pipelineConfigs.indexOf(config))
|
||||
.getFileName()
|
||||
.toString();
|
||||
name = filename.substring(0, filename.lastIndexOf('.'));
|
||||
}
|
||||
Map<String, String> configWithName = new HashMap<>();
|
||||
configWithName.put("json", config);
|
||||
configWithName.put("name", name);
|
||||
pipelineConfigsWithNames.add(configWithName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@ -86,9 +90,6 @@ public class GeneralWebController {
|
||||
return "pipeline";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@GetMapping("/merge-pdfs")
|
||||
@Hidden
|
||||
public String mergePdfForm(Model model) {
|
||||
@ -117,7 +118,6 @@ public class GeneralWebController {
|
||||
return "multi-tool";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/remove-pages")
|
||||
@Hidden
|
||||
public String pageDeleter(Model model) {
|
||||
@ -175,7 +175,6 @@ public class GeneralWebController {
|
||||
return "multi-page-layout";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/scale-pages")
|
||||
@Hidden
|
||||
public String scalePagesFrom(Model model) {
|
||||
@ -183,7 +182,6 @@ public class GeneralWebController {
|
||||
return "scale-pages";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/split-by-size-or-count")
|
||||
@Hidden
|
||||
public String splitBySizeOrCount(Model model) {
|
||||
@ -198,9 +196,7 @@ public class GeneralWebController {
|
||||
return "overlay-pdf";
|
||||
}
|
||||
|
||||
|
||||
@Autowired
|
||||
private ResourceLoader resourceLoader;
|
||||
@Autowired private ResourceLoader resourceLoader;
|
||||
|
||||
private List<FontResource> getFontNames() {
|
||||
List<FontResource> fontNames = new ArrayList<>();
|
||||
@ -216,10 +212,12 @@ public class GeneralWebController {
|
||||
|
||||
private List<FontResource> getFontNamesFromLocation(String locationPattern) {
|
||||
try {
|
||||
Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
|
||||
Resource[] resources =
|
||||
ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
|
||||
.getResources(locationPattern);
|
||||
return Arrays.stream(resources)
|
||||
.map(resource -> {
|
||||
.map(
|
||||
resource -> {
|
||||
try {
|
||||
String filename = resource.getFilename();
|
||||
if (filename != null) {
|
||||
@ -242,23 +240,28 @@ public class GeneralWebController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public String getFormatFromExtension(String extension) {
|
||||
switch (extension) {
|
||||
case "ttf": return "truetype";
|
||||
case "woff": return "woff";
|
||||
case "woff2": return "woff2";
|
||||
case "eot": return "embedded-opentype";
|
||||
case "svg": return "svg";
|
||||
default: return ""; // or throw an exception if an unexpected extension is encountered
|
||||
case "ttf":
|
||||
return "truetype";
|
||||
case "woff":
|
||||
return "woff";
|
||||
case "woff2":
|
||||
return "woff2";
|
||||
case "eot":
|
||||
return "embedded-opentype";
|
||||
case "svg":
|
||||
return "svg";
|
||||
default:
|
||||
return ""; // or throw an exception if an unexpected extension is encountered
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class FontResource {
|
||||
private String name;
|
||||
private String extension;
|
||||
private String type;
|
||||
|
||||
public FontResource(String name, String extension) {
|
||||
this.name = name;
|
||||
this.extension = extension;
|
||||
@ -288,11 +291,8 @@ public class GeneralWebController {
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/crop")
|
||||
@Hidden
|
||||
public String cropForm(Model model) {
|
||||
@ -300,7 +300,6 @@ public class GeneralWebController {
|
||||
return "crop";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/auto-split-pdf")
|
||||
@Hidden
|
||||
public String autoSPlitPDFForm(Model model) {
|
||||
|
@ -8,6 +8,7 @@ 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
|
||||
@ -20,8 +21,6 @@ public class HomeWebController {
|
||||
return "about";
|
||||
}
|
||||
|
||||
|
||||
|
||||
@GetMapping("/")
|
||||
public String home(Model model) {
|
||||
model.addAttribute("currentPage", "home");
|
||||
@ -33,9 +32,7 @@ public class HomeWebController {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
@Autowired
|
||||
ApplicationProperties applicationProperties;
|
||||
|
||||
@Autowired ApplicationProperties applicationProperties;
|
||||
|
||||
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@ResponseBody
|
||||
@ -48,5 +45,4 @@ public class HomeWebController {
|
||||
return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Comparator;
|
||||
@ -22,6 +23,7 @@ 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;
|
||||
@ -31,10 +33,7 @@ import stirling.software.SPDF.model.ApplicationProperties;
|
||||
@Tag(name = "Info", description = "Info APIs")
|
||||
public class MetricsController {
|
||||
|
||||
|
||||
@Autowired
|
||||
ApplicationProperties applicationProperties;
|
||||
|
||||
@Autowired ApplicationProperties applicationProperties;
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
@ -43,8 +42,7 @@ public class MetricsController {
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
Boolean metricsEnabled = applicationProperties.getMetrics().getEnabled();
|
||||
if(metricsEnabled == null)
|
||||
metricsEnabled = true;
|
||||
if (metricsEnabled == null) metricsEnabled = true;
|
||||
this.metricsEnabled = metricsEnabled;
|
||||
}
|
||||
|
||||
@ -53,8 +51,10 @@ public class MetricsController {
|
||||
}
|
||||
|
||||
@GetMapping("/status")
|
||||
@Operation(summary = "Application status and version",
|
||||
description = "This endpoint returns the status of the application and its version number.")
|
||||
@Operation(
|
||||
summary = "Application status and version",
|
||||
description =
|
||||
"This endpoint returns the status of the application and its version number.")
|
||||
public ResponseEntity<?> getStatus() {
|
||||
if (!metricsEnabled) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
|
||||
@ -67,9 +67,13 @@ public class MetricsController {
|
||||
}
|
||||
|
||||
@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 ResponseEntity<?> getPageLoads(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional<String> endpoint) {
|
||||
@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 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.");
|
||||
}
|
||||
@ -86,7 +90,11 @@ public class MetricsController {
|
||||
if (!endpoint.get().startsWith("/")) {
|
||||
endpoint = Optional.of("/" + endpoint.get());
|
||||
}
|
||||
System.out.println("loads " + endpoint.get() + " vs " + meter.getId().getTag("uri"));
|
||||
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();
|
||||
@ -108,7 +116,8 @@ public class MetricsController {
|
||||
}
|
||||
|
||||
@GetMapping("/loads/all")
|
||||
@Operation(summary = "GET requests count for all endpoints",
|
||||
@Operation(
|
||||
summary = "GET requests count for all endpoints",
|
||||
description = "This endpoint returns the count of GET requests for each endpoint.")
|
||||
public ResponseEntity<?> getAllEndpointLoads() {
|
||||
if (!metricsEnabled) {
|
||||
@ -133,7 +142,8 @@ public class MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
List<EndpointCount> results = counts.entrySet().stream()
|
||||
List<EndpointCount> results =
|
||||
counts.entrySet().stream()
|
||||
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
|
||||
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
|
||||
.collect(Collectors.toList());
|
||||
@ -152,26 +162,32 @@ public class MetricsController {
|
||||
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 ResponseEntity<?> getTotalRequests(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional<String> endpoint) {
|
||||
@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 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.");
|
||||
}
|
||||
@ -205,9 +221,9 @@ public class MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/requests/all")
|
||||
@Operation(summary = "POST requests count for all endpoints",
|
||||
@Operation(
|
||||
summary = "POST requests count for all endpoints",
|
||||
description = "This endpoint returns the count of POST requests for each endpoint.")
|
||||
public ResponseEntity<?> getAllPostRequests() {
|
||||
if (!metricsEnabled) {
|
||||
@ -232,7 +248,8 @@ public class MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
List<EndpointCount> results = counts.entrySet().stream()
|
||||
List<EndpointCount> results =
|
||||
counts.entrySet().stream()
|
||||
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
|
||||
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
|
||||
.collect(Collectors.toList());
|
||||
@ -243,7 +260,6 @@ public class MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/uptime")
|
||||
public ResponseEntity<?> getUptime() {
|
||||
if (!metricsEnabled) {
|
||||
|
@ -39,7 +39,6 @@ public class OtherWebController {
|
||||
return "misc/show-javascript";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/add-page-numbers")
|
||||
@Hidden
|
||||
public String addPageNumbersForm(Model model) {
|
||||
@ -61,8 +60,6 @@ public class OtherWebController {
|
||||
return "misc/flatten";
|
||||
}
|
||||
|
||||
|
||||
|
||||
@GetMapping("/change-metadata")
|
||||
@Hidden
|
||||
public String addWatermarkForm(Model model) {
|
||||
@ -83,8 +80,11 @@ public class OtherWebController {
|
||||
if (files == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Arrays.stream(files).filter(file -> file.getName().endsWith(".traineddata")).map(file -> file.getName().replace(".traineddata", ""))
|
||||
.filter(lang -> !lang.equalsIgnoreCase("osd")).collect(Collectors.toList());
|
||||
return Arrays.stream(files)
|
||||
.filter(file -> file.getName().endsWith(".traineddata"))
|
||||
.map(file -> file.getName().replace(".traineddata", ""))
|
||||
.filter(lang -> !lang.equalsIgnoreCase("osd"))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/ocr-pdf")
|
||||
@ -98,7 +98,6 @@ public class OtherWebController {
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/add-image")
|
||||
@Hidden
|
||||
public String overlayImage(Model model) {
|
||||
@ -147,7 +146,4 @@ public class OtherWebController {
|
||||
model.addAttribute("currentPage", "auto-rename");
|
||||
return "misc/auto-rename";
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ public class SecurityWebController {
|
||||
model.addAttribute("currentPage", "add-password");
|
||||
return "security/add-password";
|
||||
}
|
||||
|
||||
@GetMapping("/change-permissions")
|
||||
@Hidden
|
||||
public String permissionsForm(Model model) {
|
||||
|
@ -13,7 +13,9 @@ public class ApiEndpoint {
|
||||
public ApiEndpoint(String name, JsonNode postNode) {
|
||||
this.name = name;
|
||||
this.parameters = new HashMap<>();
|
||||
postNode.path("parameters").forEach(paramNode -> {
|
||||
postNode.path("parameters")
|
||||
.forEach(
|
||||
paramNode -> {
|
||||
String paramName = paramNode.path("name").asText();
|
||||
parameters.put(paramName, paramNode);
|
||||
});
|
||||
@ -37,6 +39,4 @@ public class ApiEndpoint {
|
||||
public String toString() {
|
||||
return "ApiEndpoint [name=" + name + ", parameters=" + parameters + "]";
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
package stirling.software.SPDF.model;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
@ -16,7 +17,8 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
|
||||
setAuthenticated(false);
|
||||
}
|
||||
|
||||
public ApiKeyAuthenticationToken(Object principal, String apiKey, Collection<? extends GrantedAuthority> authorities) {
|
||||
public ApiKeyAuthenticationToken(
|
||||
Object principal, String apiKey, Collection<? extends GrantedAuthority> authorities) {
|
||||
super(authorities);
|
||||
this.principal = principal; // principal can be a UserDetails object
|
||||
this.credentials = apiKey;
|
||||
@ -36,7 +38,8 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
|
||||
@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.");
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot set this token to trusted. Use constructor which takes a GrantedAuthority list instead.");
|
||||
}
|
||||
super.setAuthenticated(false);
|
||||
}
|
||||
|
@ -69,7 +69,9 @@ public class ApplicationProperties {
|
||||
}
|
||||
|
||||
public AutomaticallyGenerated getAutomaticallyGenerated() {
|
||||
return automaticallyGenerated != null ? automaticallyGenerated : new AutomaticallyGenerated();
|
||||
return automaticallyGenerated != null
|
||||
? automaticallyGenerated
|
||||
: new AutomaticallyGenerated();
|
||||
}
|
||||
|
||||
public void setAutomaticallyGenerated(AutomaticallyGenerated automaticallyGenerated) {
|
||||
@ -78,9 +80,21 @@ public class ApplicationProperties {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ApplicationProperties [security=" + security + ", system=" + system + ", ui=" + ui + ", endpoints="
|
||||
+ endpoints + ", metrics=" + metrics + ", automaticallyGenerated=" + automaticallyGenerated
|
||||
+ ", autoPipeline=" + autoPipeline + "]";
|
||||
return "ApplicationProperties [security="
|
||||
+ security
|
||||
+ ", system="
|
||||
+ system
|
||||
+ ", ui="
|
||||
+ ui
|
||||
+ ", endpoints="
|
||||
+ endpoints
|
||||
+ ", metrics="
|
||||
+ metrics
|
||||
+ ", automaticallyGenerated="
|
||||
+ automaticallyGenerated
|
||||
+ ", autoPipeline="
|
||||
+ autoPipeline
|
||||
+ "]";
|
||||
}
|
||||
|
||||
public static class AutoPipeline {
|
||||
@ -98,10 +112,8 @@ public class ApplicationProperties {
|
||||
public String toString() {
|
||||
return "AutoPipeline [outputFolder=" + outputFolder + "]";
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static class Security {
|
||||
private Boolean enableLogin;
|
||||
private Boolean csrfDisabled;
|
||||
@ -109,7 +121,6 @@ public class ApplicationProperties {
|
||||
private int loginAttemptCount;
|
||||
private long loginResetTimeMinutes;
|
||||
|
||||
|
||||
public int getLoginAttemptCount() {
|
||||
return loginAttemptCount;
|
||||
}
|
||||
@ -150,11 +161,15 @@ public class ApplicationProperties {
|
||||
this.csrfDisabled = csrfDisabled;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Security [enableLogin=" + enableLogin + ", initialLogin=" + initialLogin + ", csrfDisabled="
|
||||
+ csrfDisabled + "]";
|
||||
return "Security [enableLogin="
|
||||
+ enableLogin
|
||||
+ ", initialLogin="
|
||||
+ initialLogin
|
||||
+ ", csrfDisabled="
|
||||
+ csrfDisabled
|
||||
+ "]";
|
||||
}
|
||||
|
||||
public static class InitialLogin {
|
||||
@ -180,11 +195,12 @@ public class ApplicationProperties {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "InitialLogin [username=" + username + ", password=" + (password != null && !password.isEmpty() ? "MASKED" : "NULL") + "]";
|
||||
return "InitialLogin [username="
|
||||
+ username
|
||||
+ ", password="
|
||||
+ (password != null && !password.isEmpty() ? "MASKED" : "NULL")
|
||||
+ "]";
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,9 +213,6 @@ public class ApplicationProperties {
|
||||
|
||||
private Boolean enableAlphaFunctionality;
|
||||
|
||||
|
||||
|
||||
|
||||
public Boolean getEnableAlphaFunctionality() {
|
||||
return enableAlphaFunctionality;
|
||||
}
|
||||
@ -250,13 +263,20 @@ public class ApplicationProperties {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "System [defaultLocale=" + defaultLocale + ", googlevisibility=" + googlevisibility
|
||||
+ ", rootURIPath=" + rootURIPath + ", customStaticFilePath=" + customStaticFilePath
|
||||
+ ", maxFileSize=" + maxFileSize + ", enableAlphaFunctionality=" + enableAlphaFunctionality + "]";
|
||||
return "System [defaultLocale="
|
||||
+ defaultLocale
|
||||
+ ", googlevisibility="
|
||||
+ googlevisibility
|
||||
+ ", rootURIPath="
|
||||
+ rootURIPath
|
||||
+ ", customStaticFilePath="
|
||||
+ customStaticFilePath
|
||||
+ ", maxFileSize="
|
||||
+ maxFileSize
|
||||
+ ", enableAlphaFunctionality="
|
||||
+ enableAlphaFunctionality
|
||||
+ "]";
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static class Ui {
|
||||
@ -265,8 +285,7 @@ public class ApplicationProperties {
|
||||
private String appNameNavbar;
|
||||
|
||||
public String getAppName() {
|
||||
if(appName != null && appName.trim().length() == 0)
|
||||
return null;
|
||||
if (appName != null && appName.trim().length() == 0) return null;
|
||||
return appName;
|
||||
}
|
||||
|
||||
@ -275,8 +294,7 @@ public class ApplicationProperties {
|
||||
}
|
||||
|
||||
public String getHomeDescription() {
|
||||
if(homeDescription != null && homeDescription.trim().length() == 0)
|
||||
return null;
|
||||
if (homeDescription != null && homeDescription.trim().length() == 0) return null;
|
||||
return homeDescription;
|
||||
}
|
||||
|
||||
@ -285,8 +303,7 @@ public class ApplicationProperties {
|
||||
}
|
||||
|
||||
public String getAppNameNavbar() {
|
||||
if(appNameNavbar != null && appNameNavbar.trim().length() == 0)
|
||||
return null;
|
||||
if (appNameNavbar != null && appNameNavbar.trim().length() == 0) return null;
|
||||
return appNameNavbar;
|
||||
}
|
||||
|
||||
@ -296,11 +313,16 @@ public class ApplicationProperties {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UserInterface [appName=" + appName + ", homeDescription=" + homeDescription + ", appNameNavbar=" + appNameNavbar + "]";
|
||||
return "UserInterface [appName="
|
||||
+ appName
|
||||
+ ", homeDescription="
|
||||
+ homeDescription
|
||||
+ ", appNameNavbar="
|
||||
+ appNameNavbar
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class Endpoints {
|
||||
private List<String> toRemove;
|
||||
private List<String> groupsToRemove;
|
||||
@ -325,8 +347,6 @@ public class ApplicationProperties {
|
||||
public String toString() {
|
||||
return "Endpoints [toRemove=" + toRemove + ", groupsToRemove=" + groupsToRemove + "]";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static class Metrics {
|
||||
@ -344,8 +364,6 @@ public class ApplicationProperties {
|
||||
public String toString() {
|
||||
return "Metrics [enabled=" + enabled + "]";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static class AutomaticallyGenerated {
|
||||
@ -361,8 +379,9 @@ public class ApplicationProperties {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AutomaticallyGenerated [key=" + (key != null && !key.isEmpty() ? "MASKED" : "NULL") + "]";
|
||||
}
|
||||
|
||||
return "AutomaticallyGenerated [key="
|
||||
+ (key != null && !key.isEmpty() ? "MASKED" : "NULL")
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
package stirling.software.SPDF.model;
|
||||
|
||||
public class AttemptCounter {
|
||||
private int attemptCount;
|
||||
private long lastAttemptTime;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user