1
0
mirror of https://github.com/Stirling-Tools/Stirling-PDF.git synced 2024-09-28 23:51:56 +02:00

Merge branch 'main' into changes

This commit is contained in:
Anthony Stirling 2023-12-31 13:33:10 +00:00 committed by GitHub
commit d83bd1ae94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
174 changed files with 5505 additions and 4723 deletions

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

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

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

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,14 @@
<p align="center"><img src="https://raw.githubusercontent.com/Frooodle/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1> <p align="center"><img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1>
</p> </p>
[![Docker Pulls](https://img.shields.io/docker/pulls/frooodle/s-pdf)](https://hub.docker.com/r/frooodle/s-pdf) [![Docker Pulls](https://img.shields.io/docker/pulls/frooodle/s-pdf)](https://hub.docker.com/r/frooodle/s-pdf)
[![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/Cn8pWhQRxZ) [![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/Cn8pWhQRxZ)
[![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Frooodle/Stirling-PDF/) [![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/)
[![GitHub Repo stars](https://img.shields.io/github/stars/frooodle/stirling-pdf?style=social)](https://github.com/Frooodle/stirling-pdf) [![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](https://github.com/Stirling-Tools/stirling-pdf)
[![Paypal Donate](https://img.shields.io/badge/Paypal%20Donate-yellow?style=flat&logo=paypal)](https://www.paypal.com/paypalme/froodleplex) [![Paypal Donate](https://img.shields.io/badge/Paypal%20Donate-yellow?style=flat&logo=paypal)](https://www.paypal.com/paypalme/froodleplex)
[![Github Sponser](https://img.shields.io/badge/Github%20Sponsor-yellow?style=flat&logo=github)](https://github.com/sponsors/Frooodle) [![Github Sponser](https://img.shields.io/badge/Github%20Sponsor-yellow?style=flat&logo=github)](https://github.com/sponsors/Frooodle)
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Frooodle/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af) [![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
This is a powerful locally hosted web based PDF manipulation tool using docker that allows you to perform various operations on PDF files, such as splitting merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application started as a 100% ChatGPT-made application and has evolved to include a wide range of features to handle all your PDF needs. This is a powerful locally hosted web based PDF manipulation tool using docker that allows you to perform various operations on PDF files, such as splitting merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application started as a 100% ChatGPT-made application and has evolved to include a wide range of features to handle all your PDF needs.
@ -22,10 +22,10 @@ Please feel free to submit feature requests or report bugs either through GitHub
## Features ## Features
- Dark mode support. - Dark mode support.
- Custom download options (see [here](https://github.com/Frooodle/Stirling-PDF/blob/main/images/settings.png) for example) - Custom download options (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/images/settings.png) for example)
- Parallel file processing and downloads - Parallel file processing and downloads
- API for integration with external scripts - API for integration with external scripts
- Optional Login and Authentication support (see [here](https://github.com/Frooodle/Stirling-PDF/tree/main#login-authentication) for documentation) - Optional Login and Authentication support (see [here](https://github.com/Stirling-Tools/Stirling-PDF/tree/main#login-authentication) for documentation)
## **PDF Features** ## **PDF Features**
@ -80,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. - Get all information on a PDF to view or export as JSON.
For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Frooodle/Stirling-PDF/blob/main/Endpoint-groups.md) For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) hosted by the team at adminforge.de Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) hosted by the team at adminforge.de
## Technologies used ## Technologies used
@ -96,13 +96,13 @@ Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) h
## How to use ## How to use
### Locally ### Locally
Please view https://github.com/Frooodle/Stirling-PDF/blob/main/LocalRunGuide.md Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LocalRunGuide.md
### Docker / Podman ### Docker / Podman
https://hub.docker.com/r/frooodle/s-pdf https://hub.docker.com/r/frooodle/s-pdf
Stirling PDF has 3 different versions, a Full version, Lite, and ultra-Lite. Depending on the types of features you use you may want a smaller image to save on space. Stirling PDF has 3 different versions, a Full version, Lite, and ultra-Lite. Depending on the types of features you use you may want a smaller image to save on space.
To see what the different versions offer please look at our [version mapping](https://github.com/Frooodle/Stirling-PDF/blob/main/Version-groups.md) To see what the different versions offer please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md)
For people that don't mind about space optimization just use the latest tag. For people that don't mind about space optimization just use the latest tag.
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest?label=Stirling-PDF%20Full) ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest?label=Stirling-PDF%20Full)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest-lite?label=Stirling-PDF%20Lite) ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest-lite?label=Stirling-PDF%20Lite)
@ -144,7 +144,7 @@ services:
Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "podman". Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "podman".
## Enable OCR/Compression feature ## Enable OCR/Compression feature
Please view https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md
## Want to add your own language? ## Want to add your own language?
Stirling PDF currently supports 21! Stirling PDF currently supports 21!
@ -172,7 +172,7 @@ Stirling PDF currently supports 21!
- Hindi (हिंदी) (hi_IN) - Hindi (हिंदी) (hi_IN)
If you want to add your own language to Stirling-PDF please refer If you want to add your own language to Stirling-PDF please refer
https://github.com/Frooodle/Stirling-PDF/blob/main/HowToAddNewLanguage.md https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md
And please create a PR to merge it back in so others can use it! And please create a PR to merge it back in so others can use it!
@ -224,7 +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 enabled: true # 'true' to enable Info APIs endpoints (view http://localhost:8080/swagger-ui/index.html#/API to learn more), 'false' to disable
``` ```
### Extra notes ### Extra notes
- Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Frooodle/Stirling-PDF/blob/main/Endpoint-groups.md) - Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
- customStaticFilePath. Customise static files such as the app logo by placing files in the /customFiles/static/ directory. An example of customising app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF - customStaticFilePath. Customise static files such as the app logo by placing files in the /customFiles/static/ directory. An example of customising app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF
### Environment only parameters ### Environment only parameters
@ -234,7 +234,7 @@ metrics:
## API ## API
For those wanting to use Stirling-PDFs backend API to link with their own custom scripting to edit PDFs you can view all existing API documentation For those wanting to use Stirling-PDFs backend API to link with their own custom scripting to edit PDFs you can view all existing API documentation
[here](https://app.swaggerhub.com/apis-docs/Frooodle/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF) [here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF)
## Login authentication ## Login authentication

View File

@ -5,10 +5,11 @@ plugins {
id 'org.springdoc.openapi-gradle-plugin' version '1.8.0' id 'org.springdoc.openapi-gradle-plugin' version '1.8.0'
id "io.swagger.swaggerhub" version "1.3.2" id "io.swagger.swaggerhub" version "1.3.2"
id 'edu.sc.seis.launch4j' version '3.0.5' id 'edu.sc.seis.launch4j' version '3.0.5'
id 'com.diffplug.spotless' version '6.23.3'
} }
group = 'stirling.software' group = 'stirling.software'
version = '0.18.0' version = '0.18.1'
sourceCompatibility = '17' sourceCompatibility = '17'
repositories { repositories {
@ -61,6 +62,20 @@ launch4j {
messagesInstanceAlreadyExists="Stirling-PDF is already running." messagesInstanceAlreadyExists="Stirling-PDF is already running."
} }
spotless {
java {
target project.fileTree('src/main/java')
googleJavaFormat('1.19.1').aosp().reorderImports(false)
importOrder('java', 'javax', 'org', 'com', 'net', 'io')
toggleOffOn()
trimTrailingWhitespace()
indentWithSpaces()
endWithNewline()
}
}
dependencies { dependencies {
//security updates //security updates
implementation 'ch.qos.logback:logback-classic:1.4.14' implementation 'ch.qos.logback:logback-classic:1.4.14'
@ -134,6 +149,9 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok:1.18.28' annotationProcessor 'org.projectlombok:lombok:1.18.28'
} }
tasks.withType(JavaCompile) {
dependsOn 'spotlessApply'
}
task writeVersion { task writeVersion {
def propsFile = file('src/main/resources/version.properties') def propsFile = file('src/main/resources/version.properties')

View File

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

View File

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

View File

@ -22,14 +22,14 @@ public class LibreOfficeListener {
private Process process; private Process process;
private LibreOfficeListener() { private LibreOfficeListener() {}
}
private boolean isListenerRunning() { private boolean isListenerRunning() {
try { try {
System.out.println("waiting for listener to start"); System.out.println("waiting for listener to start");
Socket socket = new Socket(); Socket socket = new Socket();
socket.connect(new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second socket.connect(
new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second
socket.close(); socket.close();
return true; return true;
} catch (IOException e) { } catch (IOException e) {
@ -49,7 +49,8 @@ public class LibreOfficeListener {
// Start a background thread to monitor the activity timeout // Start a background thread to monitor the activity timeout
executorService = Executors.newSingleThreadExecutor(); executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> { executorService.submit(
() -> {
while (true) { while (true) {
long idleTime = System.currentTimeMillis() - lastActivityTime; long idleTime = System.currentTimeMillis() - lastActivityTime;
if (idleTime >= ACTIVITY_TIMEOUT) { if (idleTime >= ACTIVITY_TIMEOUT) {
@ -92,5 +93,4 @@ public class LibreOfficeListener {
process.destroy(); process.destroy();
} }
} }
} }

View File

@ -13,13 +13,12 @@ import org.springframework.scheduling.annotation.EnableScheduling;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.config.ConfigInitializer; import stirling.software.SPDF.config.ConfigInitializer;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
@SpringBootApplication
@SpringBootApplication
@EnableScheduling @EnableScheduling
public class SPdfApplication { public class SPdfApplication {
@Autowired @Autowired private Environment env;
private Environment env;
@PostConstruct @PostConstruct
public void init() { public void init() {
@ -47,9 +46,12 @@ public class SPdfApplication {
SpringApplication app = new SpringApplication(SPdfApplication.class); SpringApplication app = new SpringApplication(SPdfApplication.class);
app.addInitializers(new ConfigInitializer()); app.addInitializers(new ConfigInitializer());
if (Files.exists(Paths.get("configs/settings.yml"))) { if (Files.exists(Paths.get("configs/settings.yml"))) {
app.setDefaultProperties(Collections.singletonMap("spring.config.additional-location", "file:configs/settings.yml")); app.setDefaultProperties(
Collections.singletonMap(
"spring.config.additional-location", "file:configs/settings.yml"));
} else { } else {
System.out.println("External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead."); System.out.println(
"External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
} }
app.run(args); app.run(args);

View File

@ -5,12 +5,11 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
public class AppConfig { public class AppConfig {
@Autowired ApplicationProperties applicationProperties;
@Autowired
ApplicationProperties applicationProperties;
@Bean(name = "loginEnabled") @Bean(name = "loginEnabled")
public boolean loginEnabled() { public boolean loginEnabled() {
@ -31,28 +30,31 @@ public class AppConfig {
@Bean(name = "homeText") @Bean(name = "homeText")
public String homeText() { public String homeText() {
return (applicationProperties.getUi().getHomeDescription() != null) ? applicationProperties.getUi().getHomeDescription() : "null"; return (applicationProperties.getUi().getHomeDescription() != null)
? applicationProperties.getUi().getHomeDescription()
: "null";
} }
@Bean(name = "navBarText") @Bean(name = "navBarText")
public String navBarText() { public String navBarText() {
String defaultNavBar = applicationProperties.getUi().getAppNameNavbar() != null ? applicationProperties.getUi().getAppNameNavbar() : applicationProperties.getUi().getAppName(); String defaultNavBar =
applicationProperties.getUi().getAppNameNavbar() != null
? applicationProperties.getUi().getAppNameNavbar()
: applicationProperties.getUi().getAppName();
return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF"; return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF";
} }
@Bean(name = "enableAlphaFunctionality") @Bean(name = "enableAlphaFunctionality")
public boolean 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") @Bean(name = "rateLimit")
public boolean rateLimit() { public boolean rateLimit() {
String appName = System.getProperty("rateLimit"); String appName = System.getProperty("rateLimit");
if (appName == null) if (appName == null) appName = System.getenv("rateLimit");
appName = System.getenv("rateLimit");
return (appName != null) ? Boolean.valueOf(appName) : false; return (appName != null) ? Boolean.valueOf(appName) : false;
} }
} }

View File

@ -16,8 +16,7 @@ import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
public class Beans implements WebMvcConfigurer { public class Beans implements WebMvcConfigurer {
@Autowired @Autowired ApplicationProperties applicationProperties;
ApplicationProperties applicationProperties;
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
@ -36,9 +35,9 @@ public class Beans implements WebMvcConfigurer {
public LocaleResolver localeResolver() { public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver(); SessionLocaleResolver slr = new SessionLocaleResolver();
String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale(); String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale();
Locale defaultLocale = Locale.UK; // Fallback to UK locale if environment variable is not set Locale defaultLocale =
Locale.UK; // Fallback to UK locale if environment variable is not set
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) { if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
Locale tempLocale = Locale.forLanguageTag(appLocaleEnv); Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
@ -47,13 +46,14 @@ public class Beans implements WebMvcConfigurer {
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
defaultLocale = tempLocale; defaultLocale = tempLocale;
} else { } else {
tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_","-")); tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_", "-"));
tempLanguageTag = tempLocale.toLanguageTag(); tempLanguageTag = tempLocale.toLanguageTag();
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
defaultLocale = tempLocale; defaultLocale = tempLocale;
} else { } else {
System.err.println("Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK."); System.err.println(
"Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK.");
} }
} }
} }
@ -61,5 +61,4 @@ public class Beans implements WebMvcConfigurer {
slr.setDefaultLocale(defaultLocale); slr.setDefaultLocale(defaultLocale);
return slr; return slr;
} }
} }

View File

@ -13,11 +13,13 @@ import jakarta.servlet.http.HttpServletResponse;
public class CleanUrlInterceptor implements HandlerInterceptor { public class CleanUrlInterceptor implements HandlerInterceptor {
private static final List<String> ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error", "file", "messageType"); private static final List<String> ALLOWED_PARAMS =
Arrays.asList(
"lang", "endpoint", "endpoints", "logout", "error", "file", "messageType");
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception { throws Exception {
String queryString = request.getQueryString(); String queryString = request.getQueryString();
if (queryString != null && !queryString.isEmpty()) { if (queryString != null && !queryString.isEmpty()) {
@ -57,12 +59,16 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
} }
@Override @Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, public void postHandle(
ModelAndView modelAndView) { HttpServletRequest request,
} HttpServletResponse response,
Object handler,
ModelAndView modelAndView) {}
@Override @Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, public void afterCompletion(
Exception ex) { HttpServletRequest request,
} HttpServletResponse response,
Object handler,
Exception ex) {}
} }

View File

@ -19,7 +19,8 @@ import java.util.stream.Collectors;
import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
public class ConfigInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { public class ConfigInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override @Override
public void initialize(ConfigurableApplicationContext applicationContext) { public void initialize(ConfigurableApplicationContext applicationContext) {
@ -40,18 +41,23 @@ public class ConfigInitializer implements ApplicationContextInitializer<Configur
Files.createDirectories(destPath.getParent()); Files.createDirectories(destPath.getParent());
// Copy the resource from classpath to the external directory // Copy the resource from classpath to the external directory
try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) { try (InputStream in =
getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
if (in != null) { if (in != null) {
Files.copy(in, destPath); Files.copy(in, destPath);
} else { } else {
throw new FileNotFoundException("Resource file not found: settings.yml.template"); throw new FileNotFoundException(
"Resource file not found: settings.yml.template");
} }
} }
} else { } else {
// If user file exists, we need to merge it with the template from the classpath // If user file exists, we need to merge it with the template from the classpath
List<String> templateLines; List<String> templateLines;
try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) { try (InputStream in =
templateLines = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)).lines() getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
templateLines =
new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.toList()); .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> userLines = Files.readAllLines(userFilePath);
List<String> mergedLines = new ArrayList<>(); List<String> mergedLines = new ArrayList<>();
boolean insideAutoGenerated = false; boolean insideAutoGenerated = false;
boolean beforeFirstKey = true; boolean beforeFirstKey = true;
Function<String, Boolean> isCommented = line -> line.trim().startsWith("#"); Function<String, Boolean> isCommented = line -> line.trim().startsWith("#");
Function<String, String> extractKey = line -> { Function<String, String> extractKey =
line -> {
String[] parts = line.split(":"); String[] parts = line.split(":");
return parts.length > 0 ? parts[0].trim().replace("#", "").trim() : ""; return parts.length > 0 ? parts[0].trim().replace("#", "").trim() : "";
}; };
@ -92,21 +100,26 @@ public class ConfigInitializer implements ApplicationContextInitializer<Configur
continue; continue;
} }
if (!key.isEmpty()) if (!key.isEmpty()) beforeFirstKey = false;
beforeFirstKey = false;
if (userKeys.contains(key)) { if (userKeys.contains(key)) {
// If user has any version (commented or uncommented) of this key, skip the // If user has any version (commented or uncommented) of this key, skip the
// template line // template line
Optional<String> userValue = userLines.stream() Optional<String> userValue =
.filter(l -> extractKey.apply(l).equalsIgnoreCase(key) && !isCommented.apply(l)).findFirst(); userLines.stream()
if (userValue.isPresent()) .filter(
mergedLines.add(userValue.get()); l ->
extractKey.apply(l).equalsIgnoreCase(key)
&& !isCommented.apply(l))
.findFirst();
if (userValue.isPresent()) mergedLines.add(userValue.get());
continue; continue;
} }
if (isCommented.apply(line) || line.trim().isEmpty() || !userKeys.contains(key)) { if (isCommented.apply(line) || line.trim().isEmpty() || !userKeys.contains(key)) {
mergedLines.add(line); // If line is commented, empty or key not present in user's file, retain the mergedLines.add(
line); // If line is commented, empty or key not present in user's file,
// retain the
// template line // template line
continue; continue;
} }
@ -116,7 +129,9 @@ public class ConfigInitializer implements ApplicationContextInitializer<Configur
// template // template
for (String userLine : userLines) { for (String userLine : userLines) {
String userKey = extractKey.apply(userLine); String userKey = extractKey.apply(userLine);
boolean isPresentInTemplate = templateLines.stream().map(extractKey) boolean isPresentInTemplate =
templateLines.stream()
.map(extractKey)
.anyMatch(templateKey -> templateKey.equalsIgnoreCase(userKey)); .anyMatch(templateKey -> templateKey.equalsIgnoreCase(userKey));
if (!isPresentInTemplate && !isCommented.apply(userLine)) { if (!isPresentInTemplate && !isCommented.apply(userLine)) {
mergedLines.add(userLine); mergedLines.add(userLine);
@ -125,5 +140,4 @@ public class ConfigInitializer implements ApplicationContextInitializer<Configur
Files.write(outputPath, mergedLines, StandardCharsets.UTF_8); Files.write(outputPath, mergedLines, StandardCharsets.UTF_8);
} }
} }

View File

@ -12,6 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Service @Service
public class EndpointConfiguration { public class EndpointConfiguration {
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class); private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
@ -32,7 +33,7 @@ public class EndpointConfiguration {
} }
public void disableEndpoint(String endpoint) { public void disableEndpoint(String endpoint) {
if(!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) { if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
logger.info("Disabling {}", endpoint); logger.info("Disabling {}", endpoint);
endpointStatuses.put(endpoint, false); endpointStatuses.put(endpoint, false);
} }
@ -85,7 +86,6 @@ public class EndpointConfiguration {
addEndpointToGroup("PageOps", "overlay-pdf"); addEndpointToGroup("PageOps", "overlay-pdf");
addEndpointToGroup("PageOps", "split-pdf-by-sections"); addEndpointToGroup("PageOps", "split-pdf-by-sections");
// Adding endpoints to "Convert" group // Adding endpoints to "Convert" group
addEndpointToGroup("Convert", "pdf-to-img"); addEndpointToGroup("Convert", "pdf-to-img");
addEndpointToGroup("Convert", "img-to-pdf"); addEndpointToGroup("Convert", "img-to-pdf");
@ -102,7 +102,6 @@ public class EndpointConfiguration {
addEndpointToGroup("Convert", "markdown-to-pdf"); addEndpointToGroup("Convert", "markdown-to-pdf");
addEndpointToGroup("Convert", "pdf-to-csv"); addEndpointToGroup("Convert", "pdf-to-csv");
// Adding endpoints to "Security" group // Adding endpoints to "Security" group
addEndpointToGroup("Security", "add-password"); addEndpointToGroup("Security", "add-password");
addEndpointToGroup("Security", "remove-password"); addEndpointToGroup("Security", "remove-password");
@ -112,7 +111,6 @@ public class EndpointConfiguration {
addEndpointToGroup("Security", "sanitize-pdf"); addEndpointToGroup("Security", "sanitize-pdf");
addEndpointToGroup("Security", "auto-redact"); addEndpointToGroup("Security", "auto-redact");
// Adding endpoints to "Other" group // Adding endpoints to "Other" group
addEndpointToGroup("Other", "ocr-pdf"); addEndpointToGroup("Other", "ocr-pdf");
addEndpointToGroup("Other", "add-image"); addEndpointToGroup("Other", "add-image");
@ -131,9 +129,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Other", "get-info-on-pdf"); addEndpointToGroup("Other", "get-info-on-pdf");
addEndpointToGroup("Other", "show-javascript"); addEndpointToGroup("Other", "show-javascript");
// CLI
//CLI
addEndpointToGroup("CLI", "compress-pdf"); addEndpointToGroup("CLI", "compress-pdf");
addEndpointToGroup("CLI", "extract-image-scans"); addEndpointToGroup("CLI", "extract-image-scans");
addEndpointToGroup("CLI", "remove-blanks"); addEndpointToGroup("CLI", "remove-blanks");
@ -150,18 +146,17 @@ public class EndpointConfiguration {
addEndpointToGroup("CLI", "html-to-pdf"); addEndpointToGroup("CLI", "html-to-pdf");
addEndpointToGroup("CLI", "url-to-pdf"); addEndpointToGroup("CLI", "url-to-pdf");
// python
//python
addEndpointToGroup("Python", "extract-image-scans"); addEndpointToGroup("Python", "extract-image-scans");
addEndpointToGroup("Python", "remove-blanks"); addEndpointToGroup("Python", "remove-blanks");
addEndpointToGroup("Python", "html-to-pdf"); addEndpointToGroup("Python", "html-to-pdf");
addEndpointToGroup("Python", "url-to-pdf"); addEndpointToGroup("Python", "url-to-pdf");
//openCV // openCV
addEndpointToGroup("OpenCV", "extract-image-scans"); addEndpointToGroup("OpenCV", "extract-image-scans");
addEndpointToGroup("OpenCV", "remove-blanks"); addEndpointToGroup("OpenCV", "remove-blanks");
//LibreOffice // LibreOffice
addEndpointToGroup("LibreOffice", "repair"); addEndpointToGroup("LibreOffice", "repair");
addEndpointToGroup("LibreOffice", "file-to-pdf"); addEndpointToGroup("LibreOffice", "file-to-pdf");
addEndpointToGroup("LibreOffice", "xlsx-to-pdf"); addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
@ -171,13 +166,12 @@ public class EndpointConfiguration {
addEndpointToGroup("LibreOffice", "pdf-to-html"); addEndpointToGroup("LibreOffice", "pdf-to-html");
addEndpointToGroup("LibreOffice", "pdf-to-xml"); addEndpointToGroup("LibreOffice", "pdf-to-xml");
// OCRmyPDF
//OCRmyPDF
addEndpointToGroup("OCRmyPDF", "compress-pdf"); addEndpointToGroup("OCRmyPDF", "compress-pdf");
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa"); addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
addEndpointToGroup("OCRmyPDF", "ocr-pdf"); addEndpointToGroup("OCRmyPDF", "ocr-pdf");
//Java // Java
addEndpointToGroup("Java", "merge-pdfs"); addEndpointToGroup("Java", "merge-pdfs");
addEndpointToGroup("Java", "remove-pages"); addEndpointToGroup("Java", "remove-pages");
addEndpointToGroup("Java", "split-pdfs"); addEndpointToGroup("Java", "split-pdfs");
@ -211,13 +205,11 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "overlay-pdf"); addEndpointToGroup("Java", "overlay-pdf");
addEndpointToGroup("Java", "split-pdf-by-sections"); addEndpointToGroup("Java", "split-pdf-by-sections");
//Javascript // Javascript
addEndpointToGroup("Javascript", "pdf-organizer"); addEndpointToGroup("Javascript", "pdf-organizer");
addEndpointToGroup("Javascript", "sign"); addEndpointToGroup("Javascript", "sign");
addEndpointToGroup("Javascript", "compare"); addEndpointToGroup("Javascript", "compare");
addEndpointToGroup("Javascript", "adjust-contrast"); addEndpointToGroup("Javascript", "adjust-contrast");
} }
private void processEnvironmentConfigs() { private void processEnvironmentConfigs() {
@ -236,6 +228,4 @@ public class EndpointConfiguration {
} }
} }
} }
} }

View File

@ -10,11 +10,11 @@ import jakarta.servlet.http.HttpServletResponse;
@Component @Component
public class EndpointInterceptor implements HandlerInterceptor { public class EndpointInterceptor implements HandlerInterceptor {
@Autowired @Autowired private EndpointConfiguration endpointConfiguration;
private EndpointConfiguration endpointConfiguration;
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception { throws Exception {
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
if (!endpointConfiguration.isEndpointEnabled(requestURI)) { if (!endpointConfiguration.isEndpointEnabled(requestURI)) {

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;

View File

@ -8,6 +8,7 @@ import org.springframework.web.filter.OncePerRequestFilter;
import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.MeterRegistry;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@ -24,20 +25,34 @@ public class MetricsFilter extends OncePerRequestFilter {
} }
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException { throws ServletException, IOException {
String uri = request.getRequestURI(); String uri = request.getRequestURI();
// System.out.println("uri="+uri + ", method=" + request.getMethod() ); // System.out.println("uri="+uri + ", method=" + request.getMethod() );
// Ignore static resources // Ignore static resources
if (!(uri.startsWith("/js") || uri.startsWith("/v1/api-docs") || uri.endsWith("robots.txt") if (!(uri.startsWith("/js")
|| uri.startsWith("/images") || uri.startsWith("/images")|| uri.endsWith(".png") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".map") || uri.startsWith("/v1/api-docs")
|| uri.endsWith(".svg") || uri.endsWith(".js") || uri.contains("swagger") || uri.endsWith("robots.txt")
|| uri.startsWith("/api/v1/info") || uri.startsWith("/site.webmanifest") || uri.startsWith("/fonts") || uri.startsWith("/pdfjs") )) { || 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")
Counter counter = Counter.builder("http.requests").tag("uri", uri).tag("method", request.getMethod()) .tag("uri", uri)
.tag("method", request.getMethod())
.register(meterRegistry); .register(meterRegistry);
counter.increment(); counter.increment();
@ -46,5 +61,4 @@ public class MetricsFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }
} }

View File

@ -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.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.security.SecurityScheme;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
public class OpenApiConfig { public class OpenApiConfig {
@Autowired @Autowired ApplicationProperties applicationProperties;
ApplicationProperties applicationProperties;
@Bean @Bean
public OpenAPI customOpenAPI() { public OpenAPI customOpenAPI() {
@ -24,19 +24,30 @@ public class OpenApiConfig {
version = "1.0.0"; // default version if all else fails version = "1.0.0"; // default version if all else fails
} }
SecurityScheme apiKeyScheme = new SecurityScheme().type(SecurityScheme.Type.APIKEY).in(SecurityScheme.In.HEADER) SecurityScheme apiKeyScheme =
new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)
.name("X-API-KEY"); .name("X-API-KEY");
if (!applicationProperties.getSecurity().getEnableLogin()) { if (!applicationProperties.getSecurity().getEnableLogin()) {
return new OpenAPI().components(new Components()) return new OpenAPI()
.info(new Info().title("Stirling PDF API").version(version).description( .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.")); "API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."));
} else { } else {
return new OpenAPI().components(new Components().addSecuritySchemes("apiKey", apiKeyScheme)) return new OpenAPI()
.info(new Info().title("Stirling PDF API").version(version).description( .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.")) "API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."))
.addSecurityItem(new SecurityRequirement().addList("apiKey")); .addSecurityItem(new SecurityRequirement().addList("apiKey"));
} }
} }
} }

View File

@ -1,6 +1,5 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
@ -17,4 +16,3 @@ public class StartupApplicationListener implements ApplicationListener<ContextRe
startTime = LocalDateTime.now(); startTime = LocalDateTime.now();
} }
} }

View File

@ -9,8 +9,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration @Configuration
public class WebMvcConfig implements WebMvcConfigurer { public class WebMvcConfig implements WebMvcConfigurer {
@Autowired @Autowired private EndpointInterceptor endpointInterceptor;
private EndpointInterceptor endpointInterceptor;
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
@ -22,6 +21,6 @@ public class WebMvcConfig implements WebMvcConfigurer {
// Handler for external static resources // Handler for external static resources
registry.addResourceHandler("/**") registry.addResourceHandler("/**")
.addResourceLocations("file:customFiles/static/", "classpath:/static/"); .addResourceLocations("file:customFiles/static/", "classpath:/static/");
//.setCachePeriod(0); // Optional: disable caching // .setCachePeriod(0); // Optional: disable caching
} }
} }

View File

@ -8,6 +8,7 @@ import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource; import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory; import org.springframework.core.io.support.PropertySourceFactory;
public class YamlPropertySourceFactory implements PropertySourceFactory { public class YamlPropertySourceFactory implements PropertySourceFactory {
@Override @Override
@ -18,6 +19,7 @@ public class YamlPropertySourceFactory implements PropertySourceFactory {
Properties properties = factory.getObject(); Properties properties = factory.getObject();
return new PropertiesPropertySource(encodedResource.getResource().getFilename(), properties); return new PropertiesPropertySource(
encodedResource.getResource().getFilename(), properties);
} }
} }

View File

@ -12,11 +12,11 @@ import org.springframework.stereotype.Component;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@Component @Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired @Autowired private final LoginAttemptService loginAttemptService;
private final LoginAttemptService loginAttemptService;
@Autowired @Autowired
public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) { public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) {
@ -24,13 +24,16 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
} }
@Override @Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException { throws IOException, ServletException {
String ip = request.getRemoteAddr(); String ip = request.getRemoteAddr();
logger.error("Failed login attempt from IP: " + ip); logger.error("Failed login attempt from IP: " + ip);
String username = request.getParameter("username"); String username = request.getParameter("username");
if(loginAttemptService.loginAttemptCheck(username)) { if (loginAttemptService.loginAttemptCheck(username)) {
setDefaultFailureUrl("/login?error=locked"); setDefaultFailureUrl("/login?error=locked");
} else { } else {
@ -41,7 +44,6 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
} }
} }
super.onAuthenticationFailure(request, response, exception); super.onAuthenticationFailure(request, response, exception);
} }
} }

View File

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

View File

@ -20,28 +20,33 @@ import stirling.software.SPDF.repository.UserRepository;
@Service @Service
public class CustomUserDetailsService implements UserDetailsService { public class CustomUserDetailsService implements UserDetailsService {
@Autowired @Autowired private UserRepository userRepository;
private UserRepository userRepository;
@Autowired @Autowired private LoginAttemptService loginAttemptService;
private LoginAttemptService loginAttemptService;
@Override @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username) User user =
.orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + username)); userRepository
.findByUsername(username)
.orElseThrow(
() ->
new UsernameNotFoundException(
"No user found with username: " + username));
if (loginAttemptService.isBlocked(username)) { 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( return new org.springframework.security.core.userdetails.User(
user.getUsername(), user.getUsername(),
user.getPassword(), user.getPassword(),
user.isEnabled(), user.isEnabled(),
true, true, true, true,
getAuthorities(user.getAuthorities()) true,
); true,
getAuthorities(user.getAuthorities()));
} }
private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) { private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) {

View File

@ -20,12 +20,12 @@ import stirling.software.SPDF.utils.RequestUriUtils;
@Component @Component
public class FirstLoginFilter extends OncePerRequestFilter { public class FirstLoginFilter extends OncePerRequestFilter {
@Autowired @Autowired @Lazy private UserService userService;
@Lazy
private UserService userService;
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String method = request.getMethod(); String method = request.getMethod();
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
// Check if the request is for static resources // Check if the request is for static resources
@ -40,7 +40,10 @@ public class FirstLoginFilter extends OncePerRequestFilter {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
Optional<User> user = userService.findByUsername(authentication.getName()); Optional<User> user = userService.findByUsername(authentication.getName());
if ("GET".equalsIgnoreCase(method) && user.isPresent() && user.get().isFirstLogin() && !"/change-creds".equals(requestURI)) { if ("GET".equalsIgnoreCase(method)
&& user.isPresent()
&& user.get().isFirstLogin()
&& !"/change-creds".equals(requestURI)) {
response.sendRedirect("/change-creds"); response.sendRedirect("/change-creds");
return; return;
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -13,7 +14,8 @@ import stirling.software.SPDF.utils.RequestUriUtils;
public class IPRateLimitingFilter implements Filter { public class IPRateLimitingFilter implements Filter {
private final ConcurrentHashMap<String, AtomicInteger> requestCounts = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, AtomicInteger> requestCounts =
new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, AtomicInteger> getCounts = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, AtomicInteger> getCounts = new ConcurrentHashMap<>();
private final int maxRequests; private final int maxRequests;
private final int maxGetRequests; private final int maxGetRequests;
@ -24,7 +26,8 @@ public class IPRateLimitingFilter implements Filter {
} }
@Override @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) { if (request instanceof HttpServletRequest) {
HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletRequest httpRequest = (HttpServletRequest) request;
String method = httpRequest.getMethod(); String method = httpRequest.getMethod();

View File

@ -13,39 +13,40 @@ import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
@Component @Component
public class InitialSecuritySetup { public class InitialSecuritySetup {
@Autowired @Autowired private UserService userService;
private UserService userService;
@Autowired ApplicationProperties applicationProperties;
@Autowired
ApplicationProperties applicationProperties;
@PostConstruct @PostConstruct
public void init() { public void init() {
if (!userService.hasUsers()) { if (!userService.hasUsers()) {
String initialUsername =
String initialUsername = applicationProperties.getSecurity().getInitialLogin().getUsername(); applicationProperties.getSecurity().getInitialLogin().getUsername();
String initialPassword = applicationProperties.getSecurity().getInitialLogin().getPassword(); String initialPassword =
applicationProperties.getSecurity().getInitialLogin().getPassword();
if (initialUsername != null && initialPassword != null) { if (initialUsername != null && initialPassword != null) {
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
} else { } else {
initialUsername = "admin"; initialUsername = "admin";
initialPassword = "stirling"; 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())) { 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()); userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
} }
} }
@PostConstruct @PostConstruct
public void initSecretKey() throws IOException { public void initSecretKey() throws IOException {
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey(); String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();

View File

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

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;

View File

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

View File

@ -19,25 +19,22 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.model.ApiKeyAuthenticationToken; import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
@Component @Component
public class UserAuthenticationFilter extends OncePerRequestFilter { public class UserAuthenticationFilter extends OncePerRequestFilter {
@Autowired @Autowired private UserDetailsService userDetailsService;
private UserDetailsService userDetailsService;
@Autowired
@Lazy
private UserService userService;
@Autowired @Lazy private UserService userService;
@Autowired @Autowired
@Qualifier("loginEnabled") @Qualifier("loginEnabled")
public boolean loginEnabledValue; public boolean loginEnabledValue;
@Override @Override
protected void doFilterInternal(HttpServletRequest request, protected void doFilterInternal(
HttpServletResponse response, HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
FilterChain filterChain) throws ServletException, IOException { throws ServletException, IOException {
if (!loginEnabledValue) { if (!loginEnabledValue) {
// If login is not enabled, just pass all requests without authentication // If login is not enabled, just pass all requests without authentication
@ -52,15 +49,17 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
String apiKey = request.getHeader("X-API-Key"); String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && !apiKey.trim().isEmpty()) { if (apiKey != null && !apiKey.trim().isEmpty()) {
try { try {
// Use API key to authenticate. This requires you to have an authentication provider for API keys. // Use API key to authenticate. This requires you to have an authentication
// provider for API keys.
UserDetails userDetails = userService.loadUserByApiKey(apiKey); UserDetails userDetails = userService.loadUserByApiKey(apiKey);
if(userDetails == null) if (userDetails == null) {
{
response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Invalid API Key."); response.getWriter().write("Invalid API Key.");
return; return;
} }
authentication = new ApiKeyAuthenticationToken(userDetails, apiKey, userDetails.getAuthorities()); authentication =
new ApiKeyAuthenticationToken(
userDetails, apiKey, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (AuthenticationException e) { } catch (AuthenticationException e) {
// If API key authentication fails, deny the request // If API key authentication fails, deny the request
@ -76,12 +75,14 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
String method = request.getMethod(); String method = request.getMethod();
String contextPath = request.getContextPath(); String contextPath = request.getContextPath();
if ("GET".equalsIgnoreCase(method) && ! (contextPath + "/login").equals(requestURI)) { if ("GET".equalsIgnoreCase(method) && !(contextPath + "/login").equals(requestURI)) {
response.sendRedirect(contextPath + "/login"); // redirect to the login page response.sendRedirect(contextPath + "/login"); // redirect to the login page
return; return;
} else { } else {
response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().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; return;
} }
} }
@ -114,5 +115,4 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
return false; return false;
} }
} }

View File

@ -20,28 +20,29 @@ import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket; import io.github.bucket4j.Bucket;
import io.github.bucket4j.ConsumptionProbe; import io.github.bucket4j.ConsumptionProbe;
import io.github.bucket4j.Refill; import io.github.bucket4j.Refill;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
@Component @Component
public class UserBasedRateLimitingFilter extends OncePerRequestFilter { public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>(); private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>();
private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>(); private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
@Autowired @Autowired private UserDetailsService userDetailsService;
private UserDetailsService userDetailsService;
@Autowired @Autowired
@Qualifier("rateLimit") @Qualifier("rateLimit")
public boolean rateLimit; public boolean rateLimit;
@Override @Override
protected void doFilterInternal(HttpServletRequest request, protected void doFilterInternal(
HttpServletResponse response, HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
FilterChain filterChain) throws ServletException, IOException { throws ServletException, IOException {
if (!rateLimit) { if (!rateLimit) {
// If rateLimit is not enabled, just pass all requests without rate limiting // If rateLimit is not enabled, just pass all requests without rate limiting
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
@ -60,7 +61,8 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
// Check for API key in the request headers // Check for API key in the request headers
String apiKey = request.getHeader("X-API-Key"); String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && !apiKey.trim().isEmpty()) { if (apiKey != null && !apiKey.trim().isEmpty()) {
identifier = "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames identifier =
"API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
} else { } else {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
@ -74,14 +76,27 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
identifier = request.getRemoteAddr(); identifier = request.getRemoteAddr();
} }
Role userRole = getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication()); Role userRole =
getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
if (request.getHeader("X-API-Key") != null) { if (request.getHeader("X-API-Key") != null) {
// It's an API call // It's an API call
processRequest(userRole.getApiCallsPerDay(), identifier, apiBuckets, request, response, filterChain); processRequest(
userRole.getApiCallsPerDay(),
identifier,
apiBuckets,
request,
response,
filterChain);
} else { } else {
// It's a Web UI call // It's a Web UI call
processRequest(userRole.getWebCallsPerDay(), identifier, webBuckets, request, response, filterChain); processRequest(
userRole.getWebCallsPerDay(),
identifier,
webBuckets,
request,
response,
filterChain);
} }
} }
@ -98,8 +113,13 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
throw new IllegalStateException("User does not have a valid role."); throw new IllegalStateException("User does not have a valid role.");
} }
private void processRequest(int limitPerDay, String identifier, Map<String, Bucket> buckets, private void processRequest(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) int limitPerDay,
String identifier,
Map<String, Bucket> buckets,
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws IOException, ServletException { throws IOException, ServletException {
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay)); Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay));
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1); ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
@ -116,10 +136,8 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
} }
private Bucket createUserBucket(int limitPerDay) { private Bucket createUserBucket(int limitPerDay) {
Bandwidth limit = Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1))); Bandwidth limit =
Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1)));
return Bucket.builder().addLimit(limit).build(); return Bucket.builder().addLimit(limit).build();
} }
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -21,14 +22,13 @@ import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository; import stirling.software.SPDF.repository.UserRepository;
@Service @Service
public class UserService implements UserServiceInterface{ public class UserService implements UserServiceInterface {
@Autowired @Autowired private UserRepository userRepository;
private UserRepository userRepository;
@Autowired @Autowired private PasswordEncoder passwordEncoder;
private PasswordEncoder passwordEncoder;
public Authentication getAuthentication(String apiKey) { public Authentication getAuthentication(String apiKey) {
User user = getUserByApiKey(apiKey); User user = getUserByApiKey(apiKey);
@ -49,8 +49,6 @@ public class UserService implements UserServiceInterface{
return user.getAuthorities().stream() return user.getAuthorities().stream()
.map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority())) .map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority()))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
private String generateApiKey() { private String generateApiKey() {
@ -62,7 +60,9 @@ public class UserService implements UserServiceInterface{
} }
public User addApiKeyToUser(String username) { public User addApiKeyToUser(String username) {
User user = userRepository.findByUsername(username) User user =
userRepository
.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found")); .orElseThrow(() -> new UsernameNotFoundException("User not found"));
user.setApiKey(generateApiKey()); user.setApiKey(generateApiKey());
@ -74,7 +74,9 @@ public class UserService implements UserServiceInterface{
} }
public String getApiKeyForUser(String username) { public String getApiKeyForUser(String username) {
User user = userRepository.findByUsername(username) User user =
userRepository
.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found")); .orElseThrow(() -> new UsernameNotFoundException("User not found"));
return user.getApiKey(); return user.getApiKey();
} }
@ -95,13 +97,11 @@ public class UserService implements UserServiceInterface{
return new org.springframework.security.core.userdetails.User( return new org.springframework.security.core.userdetails.User(
user.getUsername(), user.getUsername(),
user.getPassword(), // you might not need this for API key auth user.getPassword(), // you might not need this for API key auth
getAuthorities(user) getAuthorities(user));
);
} }
return null; // or throw an exception return null; // or throw an exception
} }
public boolean validateApiKeyForUser(String username, String apiKey) { public boolean validateApiKeyForUser(String username, String apiKey) {
Optional<User> userOpt = userRepository.findByUsername(username); Optional<User> userOpt = userRepository.findByUsername(username);
return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey); return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey);
@ -161,8 +161,8 @@ public class UserService implements UserServiceInterface{
User user = userOpt.get(); User user = userOpt.get();
Map<String, String> settingsMap = user.getSettings(); Map<String, String> settingsMap = user.getSettings();
if(settingsMap == null) { if (settingsMap == null) {
settingsMap = new HashMap<String,String>(); settingsMap = new HashMap<String, String>();
} }
settingsMap.clear(); settingsMap.clear();
settingsMap.putAll(updates); settingsMap.putAll(updates);
@ -191,7 +191,6 @@ public class UserService implements UserServiceInterface{
userRepository.save(user); userRepository.save(user);
} }
public boolean isPasswordCorrect(User user, String currentPassword) { public boolean isPasswordCorrect(User user, String currentPassword) {
return passwordEncoder.matches(currentPassword, user.getPassword()); return passwordEncoder.matches(currentPassword, user.getPassword());
} }

View File

@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.CropPdfForm; import stirling.software.SPDF.model.api.general.CropPdfForm;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -31,22 +32,22 @@ public class CropController {
private static final Logger logger = LoggerFactory.getLogger(CropController.class); private static final Logger logger = LoggerFactory.getLogger(CropController.class);
@PostMapping(value = "/crop", consumes = "multipart/form-data") @PostMapping(value = "/crop", consumes = "multipart/form-data")
@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO") @Operation(
public ResponseEntity<byte[]> cropPdf(@ModelAttribute CropPdfForm form) summary = "Crops a PDF document",
throws IOException { description =
"This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> cropPdf(@ModelAttribute CropPdfForm form) throws IOException {
PDDocument sourceDocument =
PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()));
PDDocument newDocument = new PDDocument();
int totalPages = sourceDocument.getNumberOfPages();
PDDocument sourceDocument = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes())); LayerUtility layerUtility = new LayerUtility(newDocument);
PDDocument newDocument = new PDDocument(); for (int i = 0; i < totalPages; i++) {
int totalPages = sourceDocument.getNumberOfPages();
LayerUtility layerUtility = new LayerUtility(newDocument);
for (int i = 0; i < totalPages; i++) {
PDPage sourcePage = sourceDocument.getPage(i); PDPage sourcePage = sourceDocument.getPage(i);
// Create a new page with the size of the source page // Create a new page with the size of the source page
@ -71,16 +72,19 @@ for (int i = 0; i < totalPages; i++) {
contentStream.close(); contentStream.close();
// Now, set the new page's media box to the cropped size // 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();
newDocument.save(baos);
newDocument.close();
sourceDocument.close();
byte[] pdfContent = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(pdfContent, form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf");
} }
ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDocument.save(baos);
newDocument.close();
sourceDocument.close();
byte[] pdfContent = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(
pdfContent,
form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_cropped.pdf");
}
} }

View File

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

View File

@ -1,6 +1,5 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.awt.Color; import java.awt.Color;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
@ -23,6 +22,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest; import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -36,20 +36,25 @@ public class MultiPageLayoutController {
@PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data") @PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data")
@Operation( @Operation(
summary = "Merge multiple pages of a PDF document into a single page", summary = "Merge multiple pages of a PDF document into a single page",
description = "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO" description =
) "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(@ModelAttribute MergeMultiplePagesRequest request) public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(
throws IOException { @ModelAttribute MergeMultiplePagesRequest request) throws IOException {
int pagesPerSheet = request.getPagesPerSheet(); int pagesPerSheet = request.getPagesPerSheet();
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
boolean addBorder = request.isAddBorder(); boolean addBorder = request.isAddBorder();
if (pagesPerSheet != 2 && pagesPerSheet != 3 && pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) { 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"); 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); int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet);
PDDocument sourceDocument = PDDocument.load(file.getInputStream()); PDDocument sourceDocument = PDDocument.load(file.getInputStream());
@ -61,7 +66,9 @@ public class MultiPageLayoutController {
float cellWidth = newPage.getMediaBox().getWidth() / cols; float cellWidth = newPage.getMediaBox().getWidth() / cols;
float cellHeight = newPage.getMediaBox().getHeight() / rows; 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); LayerUtility layerUtility = new LayerUtility(newDocument);
float borderThickness = 1.5f; // Specify border thickness as required float borderThickness = 1.5f; // Specify border thickness as required
@ -74,7 +81,13 @@ public class MultiPageLayoutController {
contentStream.close(); contentStream.close();
newPage = new PDPage(PDRectangle.A4); newPage = new PDPage(PDRectangle.A4);
newDocument.addPage(newPage); 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); PDPage sourcePage = sourceDocument.getPage(i);
@ -83,12 +96,16 @@ public class MultiPageLayoutController {
float scaleHeight = cellHeight / rect.getHeight(); float scaleHeight = cellHeight / rect.getHeight();
float scale = Math.min(scaleWidth, scaleHeight); 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 rowIndex = adjustedPageIndex / cols;
int colIndex = adjustedPageIndex % cols; int colIndex = adjustedPageIndex % cols;
float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2; 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.saveGraphicsState();
contentStream.transform(Matrix.getTranslateInstance(x, y)); contentStream.transform(Matrix.getTranslateInstance(x, y));
@ -99,7 +116,7 @@ public class MultiPageLayoutController {
contentStream.restoreGraphicsState(); contentStream.restoreGraphicsState();
if(addBorder) { if (addBorder) {
// Draw border around each page // Draw border around each page
float borderX = colIndex * cellWidth; float borderX = colIndex * cellWidth;
float borderY = newPage.getMediaBox().getHeight() - (rowIndex + 1) * cellHeight; float borderY = newPage.getMediaBox().getHeight() - (rowIndex + 1) * cellHeight;
@ -108,7 +125,6 @@ public class MultiPageLayoutController {
} }
} }
contentStream.close(); // Close the final content stream contentStream.close(); // Close the final content stream
sourceDocument.close(); sourceDocument.close();
@ -117,8 +133,8 @@ public class MultiPageLayoutController {
newDocument.close(); newDocument.close();
byte[] result = baos.toByteArray(); byte[] result = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf"); return WebResponseUtils.bytesToWebResponse(
result,
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf");
} }
} }

View File

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

View File

@ -17,11 +17,13 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.SortTypes; import stirling.software.SPDF.model.SortTypes;
import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.model.api.general.RearrangePagesRequest; import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
@ -30,8 +32,11 @@ public class RearrangePagesPDFController {
private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class); private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class);
@PostMapping(consumes = "multipart/form-data", value = "/remove-pages") @PostMapping(consumes = "multipart/form-data", value = "/remove-pages")
@Operation(summary = "Remove pages from a PDF file", description = "This endpoint removes specified pages from a given PDF file. Users can provide a comma-separated list of page numbers or ranges to delete. Input:PDF Output:PDF Type:SISO") @Operation(
public ResponseEntity<byte[]> deletePages(@ModelAttribute PDFWithPageNums request ) summary = "Remove pages from a PDF file",
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 { throws IOException {
MultipartFile pdfFile = request.getFileInput(); MultipartFile pdfFile = request.getFileInput();
@ -42,22 +47,20 @@ public class RearrangePagesPDFController {
// Split the page order string into an array of page numbers or range of numbers // Split the page order string into an array of page numbers or range of numbers
String[] pageOrderArr = pagesToDelete.split(","); 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--) { for (int i = pagesToRemove.size() - 1; i >= 0; i--) {
int pageIndex = pagesToRemove.get(i); int pageIndex = pagesToRemove.get(i);
document.removePage(pageIndex); document.removePage(pageIndex);
} }
return WebResponseUtils.pdfDocToWebResponse(document, return WebResponseUtils.pdfDocToWebResponse(
document,
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf"); pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf");
} }
private List<Integer> removeFirst(int totalPages) { private List<Integer> removeFirst(int totalPages) {
if (totalPages <= 1) if (totalPages <= 1) return new ArrayList<>();
return new ArrayList<>();
List<Integer> newPageOrder = new ArrayList<>(); List<Integer> newPageOrder = new ArrayList<>();
for (int i = 2; i <= totalPages; i++) { for (int i = 2; i <= totalPages; i++) {
newPageOrder.add(i - 1); newPageOrder.add(i - 1);
@ -66,8 +69,7 @@ public class RearrangePagesPDFController {
} }
private List<Integer> removeLast(int totalPages) { private List<Integer> removeLast(int totalPages) {
if (totalPages <= 1) if (totalPages <= 1) return new ArrayList<>();
return new ArrayList<>();
List<Integer> newPageOrder = new ArrayList<>(); List<Integer> newPageOrder = new ArrayList<>();
for (int i = 1; i < totalPages; i++) { for (int i = 1; i < totalPages; i++) {
newPageOrder.add(i - 1); newPageOrder.add(i - 1);
@ -76,8 +78,7 @@ public class RearrangePagesPDFController {
} }
private List<Integer> removeFirstAndLast(int totalPages) { private List<Integer> removeFirstAndLast(int totalPages) {
if (totalPages <= 2) if (totalPages <= 2) return new ArrayList<>();
return new ArrayList<>();
List<Integer> newPageOrder = new ArrayList<>(); List<Integer> newPageOrder = new ArrayList<>();
for (int i = 2; i < totalPages; i++) { for (int i = 2; i < totalPages; i++) {
newPageOrder.add(i - 1); newPageOrder.add(i - 1);
@ -167,8 +168,12 @@ public class RearrangePagesPDFController {
} }
@PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages") @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") @Operation(
public ResponseEntity<byte[]> rearrangePages(@ModelAttribute RearrangePagesRequest request) throws IOException { 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(); MultipartFile pdfFile = request.getFileInput();
String pageOrder = request.getPageNumbers(); String pageOrder = request.getPageNumbers();
String sortType = request.getCustomMode(); String sortType = request.getCustomMode();
@ -185,8 +190,8 @@ public class RearrangePagesPDFController {
} else { } else {
newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages); newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages);
} }
logger.info("newPageOrder = " +newPageOrder); logger.info("newPageOrder = " + newPageOrder);
logger.info("totalPages = " +totalPages); logger.info("totalPages = " + totalPages);
// Create a new list to hold the pages in the new order // Create a new list to hold the pages in the new order
List<PDPage> newPages = new ArrayList<>(); List<PDPage> newPages = new ArrayList<>();
for (int i = 0; i < newPageOrder.size(); i++) { for (int i = 0; i < newPageOrder.size(); i++) {
@ -203,14 +208,13 @@ public class RearrangePagesPDFController {
document.addPage(page); document.addPage(page);
} }
return WebResponseUtils.pdfDocToWebResponse(document, return WebResponseUtils.pdfDocToWebResponse(
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rearranged.pdf"); document,
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_rearranged.pdf");
} catch (IOException e) { } catch (IOException e) {
logger.error("Failed rearranging documents", e); logger.error("Failed rearranging documents", e);
return null; return null;
} }
} }
} }

View File

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

View File

@ -23,8 +23,10 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.ScalePagesRequest; import stirling.software.SPDF.model.api.general.ScalePagesRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
@ -33,8 +35,12 @@ public class ScalePagesController {
private static final Logger logger = LoggerFactory.getLogger(ScalePagesController.class); private static final Logger logger = LoggerFactory.getLogger(ScalePagesController.class);
@PostMapping(value = "/scale-pages", consumes = "multipart/form-data") @PostMapping(value = "/scale-pages", consumes = "multipart/form-data")
@Operation(summary = "Change the size of a PDF page/document", description = "This operation takes an input PDF file and the size to scale the pages to in the output PDF file. Input:PDF Output:PDF Type:SISO") @Operation(
public ResponseEntity<byte[]> scalePages(@ModelAttribute ScalePagesRequest request) throws IOException { summary = "Change the size of a PDF page/document",
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(); MultipartFile file = request.getFileInput();
String targetPDRectangle = request.getPageSize(); String targetPDRectangle = request.getPageSize();
float scaleFactor = request.getScaleFactor(); float scaleFactor = request.getScaleFactor();
@ -75,7 +81,9 @@ public class ScalePagesController {
PDPage newPage = new PDPage(targetSize); PDPage newPage = new PDPage(targetSize);
outputDocument.addPage(newPage); 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 x = (targetSize.getWidth() - sourceSize.getWidth() * scale) / 2;
float y = (targetSize.getHeight() - sourceSize.getHeight() * scale) / 2; float y = (targetSize.getHeight() - sourceSize.getHeight() * scale) / 2;
@ -92,19 +100,13 @@ public class ScalePagesController {
contentStream.close(); contentStream.close();
} }
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
outputDocument.save(baos); outputDocument.save(baos);
outputDocument.close(); outputDocument.close();
sourceDocument.close(); sourceDocument.close();
return WebResponseUtils.bytesToWebResponse(
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), baos.toByteArray(),
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf"); file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf");
} }
} }

View File

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

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@ -25,17 +26,22 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest; import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class SplitPdfBySectionsController { public class SplitPdfBySectionsController {
@PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data") @PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data")
@Operation(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") @Operation(
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfBySectionsRequest request) throws Exception { 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<>(); List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
@ -59,8 +65,6 @@ public class SplitPdfBySectionsController {
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", ""); String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
byte[] data; byte[] data;
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) { try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
int pageNum = 1; int pageNum = 1;
for (int i = 0; i < splitDocumentsBoas.size(); i++) { for (int i = 0; i < splitDocumentsBoas.size(); i++) {
@ -82,10 +86,13 @@ public class SplitPdfBySectionsController {
Files.delete(zipFile); Files.delete(zipFile);
} }
return WebResponseUtils.bytesToWebResponse(data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.bytesToWebResponse(
data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM);
} }
public List<PDDocument> splitPdfPages(PDDocument document, int horizontalDivisions, int verticalDivisions) throws IOException { public List<PDDocument> splitPdfPages(
PDDocument document, int horizontalDivisions, int verticalDivisions)
throws IOException {
List<PDDocument> splitDocuments = new ArrayList<>(); List<PDDocument> splitDocuments = new ArrayList<>();
for (PDPage originalPage : document.getPages()) { for (PDPage originalPage : document.getPages()) {
@ -103,9 +110,12 @@ public class SplitPdfBySectionsController {
PDPage subPage = new PDPage(new PDRectangle(subPageWidth, subPageHeight)); PDPage subPage = new PDPage(new PDRectangle(subPageWidth, subPageHeight));
subDoc.addPage(subPage); subDoc.addPage(subPage);
PDFormXObject form = layerUtility.importPageAsForm(document, document.getPages().indexOf(originalPage)); PDFormXObject form =
layerUtility.importPageAsForm(
document, document.getPages().indexOf(originalPage));
try (PDPageContentStream contentStream = new PDPageContentStream(subDoc, subPage)) { try (PDPageContentStream contentStream =
new PDPageContentStream(subDoc, subPage)) {
// Set clipping area and position // Set clipping area and position
float translateX = -subPageWidth * i; float translateX = -subPageWidth * i;
float translateY = height - subPageHeight * (verticalDivisions - j); float translateY = height - subPageHeight * (verticalDivisions - j);
@ -127,9 +137,4 @@ public class SplitPdfBySectionsController {
return splitDocuments; return splitDocuments;
} }
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@ -20,6 +21,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest; import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -29,19 +31,20 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class SplitPdfBySizeController { public class SplitPdfBySizeController {
@PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data") @PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data")
@Operation(summary = "Auto split PDF pages into separate documents based on size or count", description = "split PDF into multiple paged documents based on size/count, ie if 20 pages and split into 5, it does 5 documents each 4 pages\r\n" @Operation(
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") + " 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>(); List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<ByteArrayOutputStream>();
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
PDDocument sourceDocument = PDDocument.load(file.getInputStream()); PDDocument sourceDocument = PDDocument.load(file.getInputStream());
//0 = size, 1 = page count, 2 = doc count // 0 = size, 1 = page count, 2 = doc count
int type = request.getSplitType(); int type = request.getSplitType();
String value = request.getSplitValue(); String value = request.getSplitValue();
@ -115,8 +118,6 @@ public class SplitPdfBySizeController {
sourceDocument.close(); sourceDocument.close();
Path zipFile = Files.createTempFile("split_documents", ".zip"); Path zipFile = Files.createTempFile("split_documents", ".zip");
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", ""); String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
byte[] data; byte[] data;
@ -139,7 +140,8 @@ public class SplitPdfBySizeController {
Files.delete(zipFile); Files.delete(zipFile);
} }
return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} }
private ByteArrayOutputStream currentDocToByteArray(PDDocument document) throws IOException { private ByteArrayOutputStream currentDocToByteArray(PDDocument document) throws IOException {
@ -148,6 +150,4 @@ public class SplitPdfBySizeController {
document.close(); document.close();
return baos; return baos;
} }
} }

View File

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

View File

@ -30,13 +30,13 @@ import stirling.software.SPDF.model.User;
@RequestMapping("/api/v1/user") @RequestMapping("/api/v1/user")
public class UserController { public class UserController {
@Autowired @Autowired private UserService userService;
private UserService userService;
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/register") @PostMapping("/register")
public String register(@RequestParam String username, @RequestParam String password, Model model) { public String register(
if(userService.usernameExists(username)) { @RequestParam String username, @RequestParam String password, Model model) {
if (userService.usernameExists(username)) {
model.addAttribute("error", "Username already exists"); model.addAttribute("error", "Username already exists");
return "register"; return "register";
} }
@ -47,7 +47,8 @@ public class UserController {
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/change-username-and-password") @PostMapping("/change-username-and-password")
public RedirectView changeUsernameAndPassword(Principal principal, public RedirectView changeUsernameAndPassword(
Principal principal,
@RequestParam String currentPassword, @RequestParam String currentPassword,
@RequestParam String newUsername, @RequestParam String newUsername,
@RequestParam String newPassword, @RequestParam String newPassword,
@ -74,9 +75,10 @@ public class UserController {
return new RedirectView("/change-creds?messageType=usernameExists"); return new RedirectView("/change-creds?messageType=usernameExists");
} }
userService.changePassword(user, newPassword); userService.changePassword(user, newPassword);
if(newUsername != null && newUsername.length() > 0 && !user.getUsername().equals(newUsername)) { if (newUsername != null
&& newUsername.length() > 0
&& !user.getUsername().equals(newUsername)) {
userService.changeUsername(user, newUsername); userService.changeUsername(user, newUsername);
} }
userService.changeFirstUse(user, false); userService.changeFirstUse(user, false);
@ -87,10 +89,10 @@ public class UserController {
return new RedirectView("/login?messageType=credsUpdated"); return new RedirectView("/login?messageType=credsUpdated");
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/change-username") @PostMapping("/change-username")
public RedirectView changeUsername(Principal principal, public RedirectView changeUsername(
Principal principal,
@RequestParam String currentPassword, @RequestParam String currentPassword,
@RequestParam String newUsername, @RequestParam String newUsername,
HttpServletRequest request, HttpServletRequest request,
@ -116,7 +118,7 @@ public class UserController {
return new RedirectView("/account?messageType=usernameExists"); return new RedirectView("/account?messageType=usernameExists");
} }
if(newUsername != null && newUsername.length() > 0) { if (newUsername != null && newUsername.length() > 0) {
userService.changeUsername(user, newUsername); userService.changeUsername(user, newUsername);
} }
@ -128,7 +130,8 @@ public class UserController {
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/change-password") @PostMapping("/change-password")
public RedirectView changePassword(Principal principal, public RedirectView changePassword(
Principal principal,
@RequestParam String currentPassword, @RequestParam String currentPassword,
@RequestParam String newPassword, @RequestParam String newPassword,
HttpServletRequest request, HttpServletRequest request,
@ -180,10 +183,14 @@ public class UserController {
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/saveUser") @PostMapping("/admin/saveUser")
public RedirectView saveUser(@RequestParam String username, @RequestParam String password, @RequestParam String role, public RedirectView saveUser(
@RequestParam(name = "forceChange", required = false, defaultValue = "false") boolean forceChange) { @RequestParam String username,
@RequestParam String password,
@RequestParam String role,
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
boolean forceChange) {
if(userService.usernameExists(username)) { if (userService.usernameExists(username)) {
return new RedirectView("/addUsers?messageType=usernameExists"); return new RedirectView("/addUsers?messageType=usernameExists");
} }
try { try {
@ -202,7 +209,6 @@ public class UserController {
return new RedirectView("/addUsers"); // Redirect to account page after adding the user return new RedirectView("/addUsers"); // Redirect to account page after adding the user
} }
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/deleteUser/{username}") @PostMapping("/admin/deleteUser/{username}")
public String deleteUser(@PathVariable String username, Authentication authentication) { public String deleteUser(@PathVariable String username, Authentication authentication) {
@ -247,6 +253,4 @@ public class UserController {
} }
return ResponseEntity.ok(apiKey); return ResponseEntity.ok(apiKey);
} }
} }

View File

@ -9,6 +9,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.GeneralFile; import stirling.software.SPDF.model.api.GeneralFile;
import stirling.software.SPDF.utils.FileToPdf; import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -18,35 +19,30 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RequestMapping("/api/v1/convert") @RequestMapping("/api/v1/convert")
public class ConvertHtmlToPDF { public class ConvertHtmlToPDF {
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf") @PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
@Operation( @Operation(
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF", summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
description = "This endpoint takes an HTML or ZIP file input and converts it to a PDF format." description =
) "This endpoint takes an HTML or ZIP file input and converts it to a PDF format.")
public ResponseEntity<byte[]> HtmlToPdf( public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute GeneralFile request) throws Exception {
@ModelAttribute GeneralFile request)
throws Exception {
MultipartFile fileInput = request.getFileInput(); MultipartFile fileInput = request.getFileInput();
if (fileInput == null) { if (fileInput == null) {
throw new IllegalArgumentException("Please provide an HTML or ZIP file for conversion."); throw new IllegalArgumentException(
"Please provide an HTML or ZIP file for conversion.");
} }
String originalFilename = fileInput.getOriginalFilename(); String originalFilename = fileInput.getOriginalFilename();
if (originalFilename == null || (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) { if (originalFilename == null
|| (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) {
throw new IllegalArgumentException("File must be either .html or .zip format."); 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); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
} }
} }

View File

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

View File

@ -12,6 +12,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.GeneralFile; import stirling.software.SPDF.model.api.GeneralFile;
import stirling.software.SPDF.utils.FileToPdf; import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -24,10 +25,9 @@ public class ConvertMarkdownToPdf {
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf") @PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
@Operation( @Operation(
summary = "Convert a Markdown file to PDF", summary = "Convert a Markdown file to PDF",
description = "This endpoint takes a Markdown file input, converts it to HTML, and then to PDF format." description =
) "This endpoint takes a Markdown file input, converts it to HTML, and then to PDF format.")
public ResponseEntity<byte[]> markdownToPdf( public ResponseEntity<byte[]> markdownToPdf(@ModelAttribute GeneralFile request)
@ModelAttribute GeneralFile request)
throws Exception { throws Exception {
MultipartFile fileInput = request.getFileInput(); MultipartFile fileInput = request.getFileInput();
@ -48,7 +48,9 @@ public class ConvertMarkdownToPdf {
byte[] pdfBytes = FileToPdf.convertHtmlToPdf(htmlContent.getBytes(), "converted.html"); 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); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
} }
} }

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest; import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
@ -28,13 +29,14 @@ public class ConvertWebsiteToPDF {
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf") @PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
@Operation( @Operation(
summary = "Convert a URL to a PDF", summary = "Convert a URL to a PDF",
description = "This endpoint fetches content from a URL and converts it to a PDF format. Input:N/A Output:PDF Type:SISO" description =
) "This endpoint fetches content from a URL and converts it to a PDF format. Input:N/A Output:PDF Type:SISO")
public ResponseEntity<byte[]> urlToPdf(@ModelAttribute UrlToPdfRequest request) throws IOException, InterruptedException { public ResponseEntity<byte[]> urlToPdf(@ModelAttribute UrlToPdfRequest request)
throws IOException, InterruptedException {
String URL = request.getUrlInput(); String URL = request.getUrlInput();
// Validate the URL format // Validate the URL format
if(!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) { if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
throw new IllegalArgumentException("Invalid URL format provided."); throw new IllegalArgumentException("Invalid URL format provided.");
} }
Path tempOutputFile = null; Path tempOutputFile = null;
@ -49,12 +51,13 @@ public class ConvertWebsiteToPDF {
command.add(URL); command.add(URL);
command.add(tempOutputFile.toString()); 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 // Read the optimized PDF file
pdfBytes = Files.readAllBytes(tempOutputFile); pdfBytes = Files.readAllBytes(tempOutputFile);
} } finally {
finally {
// Clean up the temporary files // Clean up the temporary files
Files.delete(tempOutputFile); Files.delete(tempOutputFile);
} }
@ -66,11 +69,9 @@ public class ConvertWebsiteToPDF {
private String convertURLToFileName(String url) { private String convertURLToFileName(String url) {
String safeName = url.replaceAll("[^a-zA-Z0-9]", "_"); String safeName = url.replaceAll("[^a-zA-Z0-9]", "_");
if(safeName.length() > 50) { if (safeName.length() > 50) {
safeName = safeName.substring(0, 50); // restrict to 50 characters safeName = safeName.substring(0, 50); // restrict to 50 characters
} }
return safeName + ".pdf"; return safeName + ".pdf";
} }
} }

View File

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

View File

@ -14,6 +14,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFComparisonAndCount; import stirling.software.SPDF.model.api.PDFComparisonAndCount;
import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.model.api.filter.ContainsTextRequest; import stirling.software.SPDF.model.api.filter.ContainsTextRequest;
@ -29,21 +30,27 @@ import stirling.software.SPDF.utils.WebResponseUtils;
public class FilterController { public class FilterController {
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text") @PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text")
@Operation(summary = "Checks if a PDF contains set text, returns true if does", description = "Input:PDF Output:Boolean Type:SISO") @Operation(
public ResponseEntity<byte[]> containsText(@ModelAttribute ContainsTextRequest request) throws IOException, InterruptedException { summary = "Checks if a PDF contains set text, returns true if does",
description = "Input:PDF Output:Boolean Type:SISO")
public ResponseEntity<byte[]> containsText(@ModelAttribute ContainsTextRequest request)
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
String text = request.getText(); String text = request.getText();
String pageNumber = request.getPageNumbers(); String pageNumber = request.getPageNumbers();
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
if (PdfUtils.hasText(pdfDocument, pageNumber, text)) if (PdfUtils.hasText(pdfDocument, pageNumber, text))
return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename()); return WebResponseUtils.pdfDocToWebResponse(
pdfDocument, inputFile.getOriginalFilename());
return null; return null;
} }
// TODO // TODO
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image") @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) public ResponseEntity<byte[]> containsImage(@ModelAttribute PDFWithPageNums request)
throws IOException, InterruptedException { throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
@ -51,13 +58,17 @@ public class FilterController {
PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
if (PdfUtils.hasImages(pdfDocument, pageNumber)) if (PdfUtils.hasImages(pdfDocument, pageNumber))
return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename()); return WebResponseUtils.pdfDocToWebResponse(
pdfDocument, inputFile.getOriginalFilename());
return null; return null;
} }
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-count") @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") @Operation(
public ResponseEntity<byte[]> pageCount(@ModelAttribute PDFComparisonAndCount request) throws IOException, InterruptedException { 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(); MultipartFile inputFile = request.getFileInput();
String pageCount = request.getPageCount(); String pageCount = request.getPageCount();
String comparator = request.getComparator(); String comparator = request.getComparator();
@ -81,14 +92,16 @@ public class FilterController {
throw new IllegalArgumentException("Invalid comparator: " + comparator); throw new IllegalArgumentException("Invalid comparator: " + comparator);
} }
if (valid) if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
return WebResponseUtils.multiPartFileToWebResponse(inputFile);
return null; return null;
} }
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-size") @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") @Operation(
public ResponseEntity<byte[]> pageSize(@ModelAttribute PageSizeRequest request) throws IOException, InterruptedException { 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(); MultipartFile inputFile = request.getFileInput();
String standardPageSize = request.getStandardPageSize(); String standardPageSize = request.getStandardPageSize();
String comparator = request.getComparator(); String comparator = request.getComparator();
@ -122,14 +135,16 @@ public class FilterController {
throw new IllegalArgumentException("Invalid comparator: " + comparator); throw new IllegalArgumentException("Invalid comparator: " + comparator);
} }
if (valid) if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
return WebResponseUtils.multiPartFileToWebResponse(inputFile);
return null; return null;
} }
@PostMapping(consumes = "multipart/form-data", value = "/filter-file-size") @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") @Operation(
public ResponseEntity<byte[]> fileSize(@ModelAttribute FileSizeRequest request) throws IOException, InterruptedException { 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(); MultipartFile inputFile = request.getFileInput();
String fileSize = request.getFileSize(); String fileSize = request.getFileSize();
String comparator = request.getComparator(); String comparator = request.getComparator();
@ -153,14 +168,16 @@ public class FilterController {
throw new IllegalArgumentException("Invalid comparator: " + comparator); throw new IllegalArgumentException("Invalid comparator: " + comparator);
} }
if (valid) if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
return WebResponseUtils.multiPartFileToWebResponse(inputFile);
return null; return null;
} }
@PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation") @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") @Operation(
public ResponseEntity<byte[]> pageRotation(@ModelAttribute PageRotationRequest request) throws IOException, InterruptedException { 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(); MultipartFile inputFile = request.getFileInput();
int rotation = request.getRotation(); int rotation = request.getRotation();
String comparator = request.getComparator(); String comparator = request.getComparator();
@ -187,10 +204,7 @@ public class FilterController {
throw new IllegalArgumentException("Invalid comparator: " + comparator); throw new IllegalArgumentException("Invalid comparator: " + comparator);
} }
if (valid) if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
return WebResponseUtils.multiPartFileToWebResponse(inputFile);
return null; return null;
} }
} }

View File

@ -19,8 +19,10 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest; import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
@ -32,13 +34,18 @@ public class AutoRenameController {
private static final int LINE_LIMIT = 11; private static final int LINE_LIMIT = 11;
@PostMapping(consumes = "multipart/form-data", value = "/auto-rename") @PostMapping(consumes = "multipart/form-data", value = "/auto-rename")
@Operation(summary = "Extract header from PDF file", description = "This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO") @Operation(
public ResponseEntity<byte[]> extractHeader(@ModelAttribute ExtractHeaderRequest request) throws Exception { summary = "Extract header from PDF file",
description =
"This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> extractHeader(@ModelAttribute ExtractHeaderRequest request)
throws Exception {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
Boolean useFirstTextAsFallback = request.isUseFirstTextAsFallback(); Boolean useFirstTextAsFallback = request.isUseFirstTextAsFallback();
PDDocument document = PDDocument.load(file.getInputStream()); PDDocument document = PDDocument.load(file.getInputStream());
PDFTextStripper reader = new PDFTextStripper() { PDFTextStripper reader =
new PDFTextStripper() {
class LineInfo { class LineInfo {
String text; String text;
float fontSize; float fontSize;
@ -92,7 +99,8 @@ public class AutoRenameController {
for (int i = 0; i < lineInfos.size(); i++) { for (int i = 0; i < lineInfos.size(); i++) {
String mergedText = lineInfos.get(i).text; String mergedText = lineInfos.get(i).text;
float fontSize = lineInfos.get(i).fontSize; float fontSize = lineInfos.get(i).fontSize;
while (i + 1 < lineInfos.size() && lineInfos.get(i + 1).fontSize == fontSize) { while (i + 1 < lineInfos.size()
&& lineInfos.get(i + 1).fontSize == fontSize) {
mergedText += " " + lineInfos.get(i + 1).text; mergedText += " " + lineInfos.get(i + 1).text;
i++; i++;
} }
@ -100,18 +108,24 @@ public class AutoRenameController {
} }
// Sort lines by font size in descending order and get the first one // Sort lines by font size in descending order and get the first one
mergedLineInfos.sort(Comparator.comparing((LineInfo li) -> li.fontSize).reversed()); mergedLineInfos.sort(
String title = mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(0).text; Comparator.comparing((LineInfo li) -> li.fontSize).reversed());
String title =
mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(0).text;
return title != null ? title : (useFirstTextAsFallback ? (mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(mergedLineInfos.size() - 1).text) : null); return title != null
? title
: (useFirstTextAsFallback
? (mergedLineInfos.isEmpty()
? null
: mergedLineInfos.get(mergedLineInfos.size() - 1)
.text)
: null);
} }
}; };
String header = reader.getText(document); String header = reader.getText(document);
// Sanitize the header string by removing characters not allowed in a filename. // Sanitize the header string by removing characters not allowed in a filename.
if (header != null && header.length() < 255) { if (header != null && header.length() < 255) {
header = header.replaceAll("[/\\\\?%*:|\"<>]", ""); header = header.replaceAll("[/\\\\?%*:|\"<>]", "");
@ -121,8 +135,4 @@ public class AutoRenameController {
return WebResponseUtils.pdfDocToWebResponse(document, file.getOriginalFilename()); return WebResponseUtils.pdfDocToWebResponse(document, file.getOriginalFilename());
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,21 +3,17 @@ package stirling.software.SPDF.controller.api.misc;
import java.awt.Color; import java.awt.Color;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp; import java.awt.image.AffineTransformOp;
//Required for image manipulation
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp; import java.awt.image.BufferedImageOp;
import java.awt.image.ConvolveOp; import java.awt.image.ConvolveOp;
import java.awt.image.Kernel; import java.awt.image.Kernel;
import java.awt.image.RescaleOp; import java.awt.image.RescaleOp;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
//Required for file input/output
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom; import java.security.SecureRandom;
//Other required classes
import java.util.Random; import java.util.Random;
//Required for image input/output
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
@ -40,6 +36,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -50,22 +47,21 @@ public class FakeScanControllerWIP {
private static final Logger logger = LoggerFactory.getLogger(FakeScanControllerWIP.class); private static final Logger logger = LoggerFactory.getLogger(FakeScanControllerWIP.class);
//TODO // TODO
@Hidden @Hidden
@PostMapping(consumes = "multipart/form-data", value = "/fakeScan") @PostMapping(consumes = "multipart/form-data", value = "/fakeScan")
@Operation( @Operation(
summary = "Repair a PDF file", summary = "Repair a PDF file",
description = "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response." description =
) "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response.")
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request) throws IOException { public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request) throws IOException {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
PDDocument document = PDDocument.load(inputFile.getBytes()); PDDocument document = PDDocument.load(inputFile.getBytes());
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);
for (int page = 0; page < document.getNumberOfPages(); ++page) for (int page = 0; page < document.getNumberOfPages(); ++page) {
{
BufferedImage image = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB); BufferedImage image = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
ImageIO.write(image, "png", new File("scanned-" + (page+1) + ".png")); ImageIO.write(image, "png", new File("scanned-" + (page + 1) + ".png"));
} }
document.close(); document.close();
@ -77,7 +73,9 @@ public class FakeScanControllerWIP {
BufferedImage sourceImage = ImageIO.read(new File("scanned-1.png")); BufferedImage sourceImage = ImageIO.read(new File("scanned-1.png"));
// Create the destination image // Create the destination image
BufferedImage destinationImage = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), sourceImage.getType()); BufferedImage destinationImage =
new BufferedImage(
sourceImage.getWidth(), sourceImage.getHeight(), sourceImage.getType());
// Apply a brightness and contrast effect based on the "scanned-ness" // Apply a brightness and contrast effect based on the "scanned-ness"
float scaleFactor = 1.0f + (scannedness / 100.0f) * 0.5f; // Between 1.0 and 1.5 float scaleFactor = 1.0f + (scannedness / 100.0f) * 0.5f; // Between 1.0 and 1.5
@ -86,10 +84,14 @@ public class FakeScanControllerWIP {
op.filter(sourceImage, destinationImage); op.filter(sourceImage, destinationImage);
// Apply a rotation effect // Apply a rotation effect
double rotationRequired = Math.toRadians((new SecureRandom().nextInt(3 - 1) + 1)); // Random angle between 1 and 3 degrees double rotationRequired =
Math.toRadians(
(new SecureRandom().nextInt(3 - 1)
+ 1)); // Random angle between 1 and 3 degrees
double locationX = destinationImage.getWidth() / 2; double locationX = destinationImage.getWidth() / 2;
double locationY = destinationImage.getHeight() / 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); AffineTransformOp rotateOp = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR);
destinationImage = rotateOp.filter(destinationImage, null); destinationImage = rotateOp.filter(destinationImage, null);
@ -100,7 +102,8 @@ public class FakeScanControllerWIP {
blurIntensity, blurIntensity, blurIntensity, blurIntensity, blurIntensity, blurIntensity,
blurIntensity, blurIntensity, blurIntensity blurIntensity, blurIntensity, blurIntensity
}; };
BufferedImageOp blurOp = new ConvolveOp(new Kernel(3, 3, matrix), ConvolveOp.EDGE_NO_OP, null); BufferedImageOp blurOp =
new ConvolveOp(new Kernel(3, 3, matrix), ConvolveOp.EDGE_NO_OP, null);
destinationImage = blurOp.filter(destinationImage, null); destinationImage = blurOp.filter(destinationImage, null);
// Add noise to the image based on the "dirtiness" // Add noise to the image based on the "dirtiness"
@ -117,15 +120,8 @@ public class FakeScanControllerWIP {
// Save the image // Save the image
ImageIO.write(destinationImage, "PNG", new File("scanned-1.png")); ImageIO.write(destinationImage, "PNG", new File("scanned-1.png"));
PDDocument documentOut = new PDDocument(); 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")); BufferedImage bim = ImageIO.read(new File("scanned-" + page + ".png"));
// Adjust the dimensions of the page // Adjust the dimensions of the page
@ -144,8 +140,8 @@ public class FakeScanControllerWIP {
documentOut.close(); documentOut.close();
// Return the optimized PDF as a response // Return the optimized PDF as a response
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scanned.pdf"; String outputFilename =
inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scanned.pdf";
return WebResponseUtils.boasToWebResponse(baos, outputFilename); return WebResponseUtils.boasToWebResponse(baos, outputFilename);
} }
} }

View File

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

View File

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

View File

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

View File

@ -21,6 +21,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.AddPageNumbersRequest; import stirling.software.SPDF.model.api.misc.AddPageNumbersRequest;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -33,8 +34,12 @@ public class PageNumbersController {
private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class); private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class);
@PostMapping(value = "/add-page-numbers", consumes = "multipart/form-data") @PostMapping(value = "/add-page-numbers", consumes = "multipart/form-data")
@Operation(summary = "Add page numbers to a PDF document", description = "This operation takes an input PDF file and adds page numbers to it. Input:PDF Output:PDF Type:SISO") @Operation(
public ResponseEntity<byte[]> addPageNumbers(@ModelAttribute AddPageNumbersRequest request) throws IOException { summary = "Add page numbers to a PDF document",
description =
"This operation takes an input PDF file and adds page numbers to it. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> addPageNumbers(@ModelAttribute AddPageNumbersRequest request)
throws IOException {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
String customMargin = request.getCustomMargin(); String customMargin = request.getCustomMargin();
int position = request.getPosition(); int position = request.getPosition();
@ -60,7 +65,6 @@ public class PageNumbersController {
marginFactor = 0.075f; marginFactor = 0.075f;
break; break;
default: default:
marginFactor = 0.035f; marginFactor = 0.035f;
break; break;
@ -68,19 +72,29 @@ public class PageNumbersController {
float fontSize = 12.0f; float fontSize = 12.0f;
PDType1Font font = PDType1Font.HELVETICA; PDType1Font font = PDType1Font.HELVETICA;
if(pagesToNumber == null || pagesToNumber.length() == 0) { if (pagesToNumber == null || pagesToNumber.length() == 0) {
pagesToNumber = "all"; pagesToNumber = "all";
} }
if(customText == null || customText.length() == 0) { if (customText == null || customText.length() == 0) {
customText = "{n}"; customText = "{n}";
} }
List<Integer> pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages()); List<Integer> pagesToNumberList =
GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages());
for (int i : pagesToNumberList) { for (int i : pagesToNumberList) {
PDPage page = document.getPage(i); PDPage page = document.getPage(i);
PDRectangle pageSize = page.getMediaBox(); PDRectangle pageSize = page.getMediaBox();
String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(document.getNumberOfPages())).replace("{filename}", file.getOriginalFilename().replaceFirst("[.][^.]+$", "")) : String.valueOf(pageNumber); String text =
customText != null
? customText
.replace("{n}", String.valueOf(pageNumber))
.replace("{total}", String.valueOf(document.getNumberOfPages()))
.replace(
"{filename}",
file.getOriginalFilename()
.replaceFirst("[.][^.]+$", ""))
: String.valueOf(pageNumber);
float x, y; float x, y;
@ -111,7 +125,9 @@ public class PageNumbersController {
break; break;
} }
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true); PDPageContentStream contentStream =
new PDPageContentStream(
document, page, PDPageContentStream.AppendMode.APPEND, true);
contentStream.beginText(); contentStream.beginText();
contentStream.setFont(font, fontSize); contentStream.setFont(font, fontSize);
contentStream.newLineAtOffset(x, y); contentStream.newLineAtOffset(x, y);
@ -126,10 +142,9 @@ public class PageNumbersController {
document.save(baos); document.save(baos);
document.close(); document.close();
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", MediaType.APPLICATION_PDF); return WebResponseUtils.bytesToWebResponse(
baos.toByteArray(),
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf",
MediaType.APPLICATION_PDF);
} }
} }

View File

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

View File

@ -17,24 +17,31 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class ShowJavascript { public class ShowJavascript {
private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class); private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class);
@PostMapping(consumes = "multipart/form-data", value = "/show-javascript") @PostMapping(consumes = "multipart/form-data", value = "/show-javascript")
@Operation(summary = "Grabs all JS from a PDF and returns a single JS file with all code", description = "desc. Input:PDF Output:JS Type:SISO") @Operation(
summary = "Grabs all JS from a PDF and returns a single JS file with all code",
description = "desc. Input:PDF Output:JS Type:SISO")
public ResponseEntity<byte[]> extractHeader(@ModelAttribute PDFFile request) throws Exception { public ResponseEntity<byte[]> extractHeader(@ModelAttribute PDFFile request) throws Exception {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
String script = ""; String script = "";
try (PDDocument document = PDDocument.load(inputFile.getInputStream())) { try (PDDocument document = PDDocument.load(inputFile.getInputStream())) {
if(document.getDocumentCatalog() != null && document.getDocumentCatalog().getNames() != null) { if (document.getDocumentCatalog() != null
PDNameTreeNode<PDActionJavaScript> jsTree = document.getDocumentCatalog().getNames().getJavaScript(); && document.getDocumentCatalog().getNames() != null) {
PDNameTreeNode<PDActionJavaScript> jsTree =
document.getDocumentCatalog().getNames().getJavaScript();
if (jsTree != null) { if (jsTree != null) {
Map<String, PDActionJavaScript> jsEntries = jsTree.getNames(); Map<String, PDActionJavaScript> jsEntries = jsTree.getNames();
@ -44,20 +51,26 @@ public class ShowJavascript {
PDActionJavaScript jsAction = entry.getValue(); PDActionJavaScript jsAction = entry.getValue();
String jsCodeStr = jsAction.getAction(); 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()) { 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");
} }
} }
} }

View File

@ -1,9 +1,11 @@
package stirling.software.SPDF.controller.api.pipeline; package stirling.software.SPDF.controller.api.pipeline;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
@ -17,11 +19,10 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContext;
import stirling.software.SPDF.SPdfApplication; import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.model.ApiEndpoint; import stirling.software.SPDF.model.ApiEndpoint;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
import org.slf4j.Logger;
@Service @Service
public class ApiDocService { public class ApiDocService {
@ -29,30 +30,26 @@ public class ApiDocService {
private static final Logger logger = LoggerFactory.getLogger(ApiDocService.class); private static final Logger logger = LoggerFactory.getLogger(ApiDocService.class);
@Autowired @Autowired private ServletContext servletContext;
private ServletContext servletContext;
private String getApiDocsUrl() { private String getApiDocsUrl() {
String contextPath = servletContext.getContextPath(); String contextPath = servletContext.getContextPath();
String port = SPdfApplication.getPort(); String port = SPdfApplication.getPort();
return "http://localhost:"+ port + contextPath + "/v1/api-docs"; return "http://localhost:" + port + contextPath + "/v1/api-docs";
} }
@Autowired(required = false)
@Autowired(required=false)
private UserServiceInterface userService; private UserServiceInterface userService;
private String getApiKeyForUser() { private String getApiKeyForUser() {
if(userService == null) if (userService == null) return "";
return "";
return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId()); return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId());
} }
JsonNode apiDocsJsonRootNode; JsonNode apiDocsJsonRootNode;
// @EventListener(ApplicationReadyEvent.class)
//@EventListener(ApplicationReadyEvent.class)
private synchronized void loadApiDocumentation() { private synchronized void loadApiDocumentation() {
String apiDocsJson = ""; String apiDocsJson = "";
try { try {
@ -64,14 +61,17 @@ public class ApiDocService {
HttpEntity<String> entity = new HttpEntity<>(headers); HttpEntity<String> entity = new HttpEntity<>(headers);
RestTemplate restTemplate = new RestTemplate(); RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class); ResponseEntity<String> response =
restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class);
apiDocsJson = response.getBody(); apiDocsJson = response.getBody();
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
apiDocsJsonRootNode = mapper.readTree(apiDocsJson); apiDocsJsonRootNode = mapper.readTree(apiDocsJson);
JsonNode paths = apiDocsJsonRootNode.path("paths"); JsonNode paths = apiDocsJsonRootNode.path("paths");
paths.fields().forEachRemaining(entry -> { paths.fields()
.forEachRemaining(
entry -> {
String path = entry.getKey(); String path = entry.getKey();
JsonNode pathNode = entry.getValue(); JsonNode pathNode = entry.getValue();
if (pathNode.has("post")) { if (pathNode.has("post")) {
@ -87,7 +87,7 @@ public class ApiDocService {
} }
public boolean isValidOperation(String operationName, Map<String, Object> parameters) { public boolean isValidOperation(String operationName, Map<String, Object> parameters) {
if(apiDocumentation.size() == 0) { if (apiDocumentation.size() == 0) {
loadApiDocumentation(); loadApiDocumentation();
} }
if (!apiDocumentation.containsKey(operationName)) { if (!apiDocumentation.containsKey(operationName)) {
@ -98,7 +98,7 @@ public class ApiDocService {
} }
public boolean isMultiInput(String operationName) { public boolean isMultiInput(String operationName) {
if(apiDocsJsonRootNode == null || apiDocumentation.size() == 0) { if (apiDocsJsonRootNode == null || apiDocumentation.size() == 0) {
loadApiDocumentation(); loadApiDocumentation();
} }
if (!apiDocumentation.containsKey(operationName)) { if (!apiDocumentation.containsKey(operationName)) {
@ -120,4 +120,3 @@ public class ApiDocService {
} }
// Model class for API Endpoint // Model class for API Endpoint

View File

@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.api.HandleDataRequest; import stirling.software.SPDF.model.api.HandleDataRequest;
@ -38,19 +39,15 @@ public class PipelineController {
final String watchedFoldersDir = "./pipeline/watchedFolders/"; final String watchedFoldersDir = "./pipeline/watchedFolders/";
final String finishedFoldersDir = "./pipeline/finishedFolders/"; final String finishedFoldersDir = "./pipeline/finishedFolders/";
@Autowired @Autowired PipelineProcessor processor;
PipelineProcessor processor;
@Autowired ApplicationProperties applicationProperties;
@Autowired @Autowired private ObjectMapper objectMapper;
ApplicationProperties applicationProperties;
@Autowired
private ObjectMapper objectMapper;
@PostMapping("/handleData") @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())) { if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST); return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
} }
@ -64,7 +61,7 @@ public class PipelineController {
logger.info("Received POST request to /handleData with {} files", files.length); logger.info("Received POST request to /handleData with {} files", files.length);
try { try {
List<Resource> inputFiles = processor.generateInputFiles(files); List<Resource> inputFiles = processor.generateInputFiles(files);
if(inputFiles == null || inputFiles.size() == 0) { if (inputFiles == null || inputFiles.size() == 0) {
return null; return null;
} }
List<Resource> outputFiles = processor.runPipelineAgainstFiles(inputFiles, config); List<Resource> outputFiles = processor.runPipelineAgainstFiles(inputFiles, config);
@ -77,8 +74,8 @@ public class PipelineController {
is.close(); is.close();
logger.info("Returning single file response..."); logger.info("Returning single file response...");
return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(), return WebResponseUtils.bytesToWebResponse(
MediaType.APPLICATION_OCTET_STREAM); bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM);
} else if (outputFiles == null) { } else if (outputFiles == null) {
return null; return null;
} }
@ -107,11 +104,11 @@ public class PipelineController {
zipOut.close(); zipOut.close();
logger.info("Returning zipped file response..."); 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) { } catch (Exception e) {
logger.error("Error handling data: ", e); logger.error("Error handling data: ", e);
return null; return null;
} }
} }
} }

View File

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

View File

@ -34,7 +34,6 @@ import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContext;
import stirling.software.SPDF.SPdfApplication; import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineOperation;
@ -45,11 +44,9 @@ public class PipelineProcessor {
private static final Logger logger = LoggerFactory.getLogger(PipelineProcessor.class); private static final Logger logger = LoggerFactory.getLogger(PipelineProcessor.class);
@Autowired private ApiDocService apiDocService;
@Autowired @Autowired(required = false)
private ApiDocService apiDocService;
@Autowired(required=false)
private UserServiceInterface userService; private UserServiceInterface userService;
@Autowired @Autowired
@ -191,6 +188,7 @@ public class PipelineProcessor {
RestTemplate restTemplate = new RestTemplate(); RestTemplate restTemplate = new RestTemplate();
// Set up headers, including API key // Set up headers, including API key
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
String apiKey = getApiKeyForUser(); String apiKey = getApiKeyForUser();
headers.add("X-API-Key", apiKey); headers.add("X-API-Key", apiKey);
@ -203,7 +201,12 @@ public class PipelineProcessor {
return restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class); return restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
} }
private List<Resource> processOutputFiles(String operation, String fileName, ResponseEntity<byte[]> response, List<Resource> newOutputFiles) throws IOException{ private List<Resource> processOutputFiles(
String operation,
String fileName,
ResponseEntity<byte[]> response,
List<Resource> newOutputFiles)
throws IOException {
// Define filename // Define filename
String newFilename; String newFilename;
if ("auto-rename".equals(operation)) { if ("auto-rename".equals(operation)) {
@ -221,7 +224,8 @@ public class PipelineProcessor {
// Unzip the file and add all the files to the new output files // Unzip the file and add all the files to the new output files
newOutputFiles.addAll(unzip(response.getBody())); newOutputFiles.addAll(unzip(response.getBody()));
} else { } else {
Resource outputResource = new ByteArrayResource(response.getBody()) { Resource outputResource =
new ByteArrayResource(response.getBody()) {
@Override @Override
public String getFilename() { public String getFilename() {
return newFilename; return newFilename;
@ -231,15 +235,14 @@ public class PipelineProcessor {
} }
return newOutputFiles; return newOutputFiles;
} }
List<Resource> generateInputFiles(File[] files) throws Exception { List<Resource> generateInputFiles(File[] files) throws Exception {
if (files == null || files.length == 0) { if (files == null || files.length == 0) {
logger.info("No files"); logger.info("No files");
return null; return null;
} }
List<Resource> outputFiles = new ArrayList<>(); List<Resource> outputFiles = new ArrayList<>();
for (File file : files) { for (File file : files) {
@ -247,7 +250,8 @@ public class PipelineProcessor {
logger.info("Reading file: " + path); // debug statement logger.info("Reading file: " + path); // debug statement
if (Files.exists(path)) { if (Files.exists(path)) {
Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) { Resource fileResource =
new ByteArrayResource(Files.readAllBytes(path)) {
@Override @Override
public String getFilename() { public String getFilename() {
return file.getName(); return file.getName();
@ -271,7 +275,8 @@ public class PipelineProcessor {
List<Resource> outputFiles = new ArrayList<>(); List<Resource> outputFiles = new ArrayList<>();
for (MultipartFile file : files) { for (MultipartFile file : files) {
Resource fileResource = new ByteArrayResource(file.getBytes()) { Resource fileResource =
new ByteArrayResource(file.getBytes()) {
@Override @Override
public String getFilename() { public String getFilename() {
return file.getOriginalFilename(); return file.getOriginalFilename();
@ -310,7 +315,8 @@ public class PipelineProcessor {
} }
final String filename = entry.getName(); final String filename = entry.getName();
Resource fileResource = new ByteArrayResource(baos.toByteArray()) { Resource fileResource =
new ByteArrayResource(baos.toByteArray()) {
@Override @Override
public String getFilename() { public String getFilename() {
return filename; return filename;
@ -330,5 +336,4 @@ public class PipelineProcessor {
logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size()); logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
return unzippedFiles; return unzippedFiles;
} }
} }

View File

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

View File

@ -53,6 +53,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest; import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -68,8 +69,12 @@ public class CertSignController {
} }
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign") @PostMapping(consumes = "multipart/form-data", value = "/cert-sign")
@Operation(summary = "Sign PDF with a Digital Certificate", description = "This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:MF-SISO") @Operation(
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) throws Exception { summary = "Sign PDF with a Digital Certificate",
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(); MultipartFile pdf = request.getFileInput();
String certType = request.getCertType(); String certType = request.getCertType();
MultipartFile privateKeyFile = request.getPrivateKeyFile(); MultipartFile privateKeyFile = request.getPrivateKeyFile();
@ -91,10 +96,13 @@ public class CertSignController {
case "PKCS12": case "PKCS12":
if (p12File != null) { if (p12File != null) {
KeyStore ks = KeyStore.getInstance("PKCS12"); KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(new ByteArrayInputStream(p12File.getBytes()), password.toCharArray()); ks.load(
new ByteArrayInputStream(p12File.getBytes()),
password.toCharArray());
String alias = ks.aliases().nextElement(); String alias = ks.aliases().nextElement();
if (!ks.isKeyEntry(alias)) { 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()); privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray());
cert = (X509Certificate) ks.getCertificate(alias); cert = (X509Certificate) ks.getCertificate(alias);
@ -103,23 +111,34 @@ public class CertSignController {
case "PEM": case "PEM":
if (privateKeyFile != null && certFile != null) { if (privateKeyFile != null && certFile != null) {
// Load private key // Load private key
KeyFactory keyFactory = KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME); KeyFactory keyFactory =
KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME);
if (isPEM(privateKeyFile.getBytes())) { if (isPEM(privateKeyFile.getBytes())) {
privateKey = keyFactory privateKey =
.generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes()))); keyFactory.generatePrivate(
new PKCS8EncodedKeySpec(
parsePEM(privateKeyFile.getBytes())));
} else { } else {
privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes())); privateKey =
keyFactory.generatePrivate(
new PKCS8EncodedKeySpec(privateKeyFile.getBytes()));
} }
// Load certificate // Load certificate
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", CertificateFactory certFactory =
BouncyCastleProvider.PROVIDER_NAME); CertificateFactory.getInstance(
"X.509", BouncyCastleProvider.PROVIDER_NAME);
if (isPEM(certFile.getBytes())) { if (isPEM(certFile.getBytes())) {
cert = (X509Certificate) certFactory cert =
.generateCertificate(new ByteArrayInputStream(parsePEM(certFile.getBytes()))); (X509Certificate)
certFactory.generateCertificate(
new ByteArrayInputStream(
parsePEM(certFile.getBytes())));
} else { } else {
cert = (X509Certificate) certFactory cert =
.generateCertificate(new ByteArrayInputStream(certFile.getBytes())); (X509Certificate)
certFactory.generateCertificate(
new ByteArrayInputStream(certFile.getBytes()));
} }
} }
break; break;
@ -154,11 +173,12 @@ public class CertSignController {
PDSignatureField signatureField = new PDSignatureField(acroForm); PDSignatureField signatureField = new PDSignatureField(acroForm);
PDAnnotationWidget widget = signatureField.getWidgets().get(0); 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); widget.setRectangle(rect);
page.getAnnotations().add(widget); page.getAnnotations().add(widget);
// Set the appearance for the signature field // Set the appearance for the signature field
PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary(); PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary();
PDAppearanceStream appearanceStream = new PDAppearanceStream(document); PDAppearanceStream appearanceStream = new PDAppearanceStream(document);
appearanceStream.setResources(new PDResources()); appearanceStream.setResources(new PDResources());
@ -166,13 +186,18 @@ public class CertSignController {
appearanceDict.setNormalAppearance(appearanceStream); appearanceDict.setNormalAppearance(appearanceStream);
widget.setAppearance(appearanceDict); widget.setAppearance(appearanceDict);
try (PDPageContentStream contentStream = new PDPageContentStream(document, appearanceStream)) { try (PDPageContentStream contentStream =
new PDPageContentStream(document, appearanceStream)) {
contentStream.beginText(); contentStream.beginText();
contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12); contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12);
contentStream.newLineAtOffset(110, 130); 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.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); contentStream.newLineAtOffset(0, -15);
if (reason != null && !reason.isEmpty()) { if (reason != null && !reason.isEmpty()) {
contentStream.showText("Reason: " + reason); contentStream.showText("Reason: " + reason);
@ -205,8 +230,8 @@ public class CertSignController {
document.addSignature(signature, signatureOptions); document.addSignature(signature, signatureOptions);
logger.info("Signature added to the PDF document"); logger.info("Signature added to the PDF document");
// External signing // External signing
ExternalSigningSupport externalSigning = document ExternalSigningSupport externalSigning =
.saveIncrementalForExternalSigning(new ByteArrayOutputStream()); document.saveIncrementalForExternalSigning(new ByteArrayOutputStream());
byte[] content = IOUtils.toByteArray(externalSigning.getContent()); byte[] content = IOUtils.toByteArray(externalSigning.getContent());
@ -214,11 +239,16 @@ public class CertSignController {
CMSTypedData cmsData = new CMSProcessableByteArray(content); CMSTypedData cmsData = new CMSProcessableByteArray(content);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA") ContentSigner signer =
.setProvider(BouncyCastleProvider.PROVIDER_NAME).build(privateKey); new JcaContentSignerBuilder("SHA256withRSA")
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(privateKey);
gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder( gen.addSignerInfoGenerator(
new JcaDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build()) new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder()
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build())
.build(signer, cert)); .build(signer, cert));
gen.addCertificates(new JcaCertStore(Collections.singletonList(cert))); gen.addCertificates(new JcaCertStore(Collections.singletonList(cert)));
@ -232,7 +262,8 @@ public class CertSignController {
// After setting the signature, return the resultant PDF // After setting the signature, return the resultant PDF
try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) { try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) {
document.save(signedPdfOutput); document.save(signedPdfOutput);
return WebResponseUtils.boasToWebResponse(signedPdfOutput, return WebResponseUtils.boasToWebResponse(
signedPdfOutput,
pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_signed.pdf"); pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_signed.pdf");
} catch (Exception e) { } catch (Exception e) {
@ -246,7 +277,8 @@ public class CertSignController {
} }
private byte[] parsePEM(byte[] content) throws IOException { 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(); return pemReader.readPemObject().getContent();
} }
@ -254,5 +286,4 @@ public class CertSignController {
String contentStr = new String(content); String contentStr = new String(content);
return contentStr.contains("-----BEGIN") && contentStr.contains("-----END"); return contentStr.contains("-----BEGIN") && contentStr.contains("-----END");
} }
} }

View File

@ -72,8 +72,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/security") @RequestMapping("/api/v1/security")
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
@ -83,12 +85,9 @@ public class GetInfoOnPDF {
@PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf") @PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf")
@Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO") @Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO")
public ResponseEntity<byte[]> getPdfInfo(@ModelAttribute PDFFile request) public ResponseEntity<byte[]> getPdfInfo(@ModelAttribute PDFFile request) throws IOException {
throws IOException {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
try ( try (PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream()); ) {
PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream());
) {
ObjectMapper objectMapper = new ObjectMapper(); ObjectMapper objectMapper = new ObjectMapper();
ObjectNode jsonOutput = objectMapper.createObjectNode(); ObjectNode jsonOutput = objectMapper.createObjectNode();
@ -101,7 +100,6 @@ public class GetInfoOnPDF {
ObjectNode encryption = objectMapper.createObjectNode(); ObjectNode encryption = objectMapper.createObjectNode();
ObjectNode other = objectMapper.createObjectNode(); ObjectNode other = objectMapper.createObjectNode();
metadata.put("Title", info.getTitle()); metadata.put("Title", info.getTitle());
metadata.put("Author", info.getAuthor()); metadata.put("Author", info.getAuthor());
metadata.put("Subject", info.getSubject()); metadata.put("Subject", info.getSubject());
@ -112,9 +110,6 @@ public class GetInfoOnPDF {
metadata.put("ModificationDate", formatDate(info.getModificationDate())); metadata.put("ModificationDate", formatDate(info.getModificationDate()));
jsonOutput.set("Metadata", metadata); jsonOutput.set("Metadata", metadata);
// Total file size of the PDF // Total file size of the PDF
long fileSizeInBytes = inputFile.getSize(); long fileSizeInBytes = inputFile.getSize();
basicInfo.put("FileSizeInBytes", fileSizeInBytes); basicInfo.put("FileSizeInBytes", fileSizeInBytes);
@ -130,7 +125,6 @@ public class GetInfoOnPDF {
int charCount = fullText.length(); int charCount = fullText.length();
basicInfo.put("CharacterCount", charCount); basicInfo.put("CharacterCount", charCount);
// Initialize the flags and types // Initialize the flags and types
boolean hasCompression = false; boolean hasCompression = false;
String compressionType = "None"; String compressionType = "None";
@ -147,25 +141,20 @@ public class GetInfoOnPDF {
} }
} }
basicInfo.put("Compression", hasCompression); basicInfo.put("Compression", hasCompression);
if(hasCompression) if (hasCompression) basicInfo.put("CompressionType", compressionType);
basicInfo.put("CompressionType", compressionType);
String language = pdfBoxDoc.getDocumentCatalog().getLanguage(); String language = pdfBoxDoc.getDocumentCatalog().getLanguage();
basicInfo.put("Language", language); basicInfo.put("Language", language);
basicInfo.put("Number of pages", pdfBoxDoc.getNumberOfPages()); basicInfo.put("Number of pages", pdfBoxDoc.getNumberOfPages());
PDDocumentCatalog catalog = pdfBoxDoc.getDocumentCatalog(); PDDocumentCatalog catalog = pdfBoxDoc.getDocumentCatalog();
String pageMode = catalog.getPageMode().name(); String pageMode = catalog.getPageMode().name();
// Document Information using PDFBox // Document Information using PDFBox
docInfoNode.put("PDF version", pdfBoxDoc.getVersion()); docInfoNode.put("PDF version", pdfBoxDoc.getVersion());
docInfoNode.put("Trapped", info.getTrapped()); docInfoNode.put("Trapped", info.getTrapped());
docInfoNode.put("Page Mode", getPageModeDescription(pageMode));; docInfoNode.put("Page Mode", getPageModeDescription(pageMode));
;
PDAcroForm acroForm = pdfBoxDoc.getDocumentCatalog().getAcroForm(); PDAcroForm acroForm = pdfBoxDoc.getDocumentCatalog().getAcroForm();
@ -177,25 +166,22 @@ public class GetInfoOnPDF {
} }
jsonOutput.set("FormFields", formFieldsNode); jsonOutput.set("FormFields", formFieldsNode);
// embeed files TODO size
if (catalog.getNames() != null) {
//embeed files TODO size
if(catalog.getNames() != null) {
PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles(); PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles();
ArrayNode embeddedFilesArray = objectMapper.createArrayNode(); ArrayNode embeddedFilesArray = objectMapper.createArrayNode();
if (efTree != null) { if (efTree != null) {
Map<String, PDComplexFileSpecification> efMap = efTree.getNames(); Map<String, PDComplexFileSpecification> efMap = efTree.getNames();
if (efMap != null) { if (efMap != null) {
for (Map.Entry<String, PDComplexFileSpecification> entry : efMap.entrySet()) { for (Map.Entry<String, PDComplexFileSpecification> entry :
efMap.entrySet()) {
ObjectNode embeddedFileNode = objectMapper.createObjectNode(); ObjectNode embeddedFileNode = objectMapper.createObjectNode();
embeddedFileNode.put("Name", entry.getKey()); embeddedFileNode.put("Name", entry.getKey());
PDEmbeddedFile embeddedFile = entry.getValue().getEmbeddedFile(); PDEmbeddedFile embeddedFile = entry.getValue().getEmbeddedFile();
if (embeddedFile != null) { if (embeddedFile != null) {
embeddedFileNode.put("FileSize", embeddedFile.getLength()); // size in bytes embeddedFileNode.put(
"FileSize", embeddedFile.getLength()); // size in bytes
} }
embeddedFilesArray.add(embeddedFileNode); embeddedFilesArray.add(embeddedFileNode);
} }
@ -204,14 +190,13 @@ public class GetInfoOnPDF {
other.set("EmbeddedFiles", embeddedFilesArray); other.set("EmbeddedFiles", embeddedFilesArray);
} }
// attachments TODO size
//attachments TODO size
ArrayNode attachmentsArray = objectMapper.createArrayNode(); ArrayNode attachmentsArray = objectMapper.createArrayNode();
for (PDPage page : pdfBoxDoc.getPages()) { for (PDPage page : pdfBoxDoc.getPages()) {
for (PDAnnotation annotation : page.getAnnotations()) { for (PDAnnotation annotation : page.getAnnotations()) {
if (annotation instanceof PDAnnotationFileAttachment) { if (annotation instanceof PDAnnotationFileAttachment) {
PDAnnotationFileAttachment fileAttachmentAnnotation = (PDAnnotationFileAttachment) annotation; PDAnnotationFileAttachment fileAttachmentAnnotation =
(PDAnnotationFileAttachment) annotation;
ObjectNode attachmentNode = objectMapper.createObjectNode(); ObjectNode attachmentNode = objectMapper.createObjectNode();
attachmentNode.put("Name", fileAttachmentAnnotation.getAttachmentName()); attachmentNode.put("Name", fileAttachmentAnnotation.getAttachmentName());
@ -223,7 +208,7 @@ public class GetInfoOnPDF {
} }
other.set("Attachments", attachmentsArray); other.set("Attachments", attachmentsArray);
//Javascript // Javascript
PDDocumentNameDictionary namesDict = catalog.getNames(); PDDocumentNameDictionary namesDict = catalog.getNames();
ArrayNode javascriptArray = objectMapper.createArrayNode(); ArrayNode javascriptArray = objectMapper.createArrayNode();
@ -254,9 +239,9 @@ public class GetInfoOnPDF {
} }
other.set("JavaScript", javascriptArray); other.set("JavaScript", javascriptArray);
// TODO size
//TODO size PDOptionalContentProperties ocProperties =
PDOptionalContentProperties ocProperties = pdfBoxDoc.getDocumentCatalog().getOCProperties(); pdfBoxDoc.getDocumentCatalog().getOCProperties();
ArrayNode layersArray = objectMapper.createArrayNode(); ArrayNode layersArray = objectMapper.createArrayNode();
if (ocProperties != null) { if (ocProperties != null) {
@ -269,16 +254,13 @@ public class GetInfoOnPDF {
other.set("Layers", layersArray); other.set("Layers", layersArray);
//TODO Security // TODO Security
PDStructureTreeRoot structureTreeRoot =
pdfBoxDoc.getDocumentCatalog().getStructureTreeRoot();
PDStructureTreeRoot structureTreeRoot = pdfBoxDoc.getDocumentCatalog().getStructureTreeRoot();
ArrayNode structureTreeArray; ArrayNode structureTreeArray;
try { try {
if(structureTreeRoot != null) { if (structureTreeRoot != null) {
structureTreeArray = exploreStructureTree(structureTreeRoot.getKids()); structureTreeArray = exploreStructureTree(structureTreeRoot.getKids());
other.set("StructureTree", structureTreeArray); other.set("StructureTree", structureTreeArray);
} }
@ -287,14 +269,21 @@ public class GetInfoOnPDF {
e.printStackTrace(); e.printStackTrace();
} }
boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A"); boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A");
boolean isPdfXCompliant = checkForStandard(pdfBoxDoc, "PDF/X"); boolean isPdfXCompliant = checkForStandard(pdfBoxDoc, "PDF/X");
boolean isPdfECompliant = checkForStandard(pdfBoxDoc, "PDF/E"); boolean isPdfECompliant = checkForStandard(pdfBoxDoc, "PDF/E");
boolean isPdfVTCompliant = checkForStandard(pdfBoxDoc, "PDF/VT"); boolean isPdfVTCompliant = checkForStandard(pdfBoxDoc, "PDF/VT");
boolean isPdfUACompliant = checkForStandard(pdfBoxDoc, "PDF/UA"); boolean isPdfUACompliant = checkForStandard(pdfBoxDoc, "PDF/UA");
boolean isPdfBCompliant = checkForStandard(pdfBoxDoc, "PDF/B"); // If you want to check for PDF/Broadcast, though this isn't an official ISO standard. boolean isPdfBCompliant =
boolean isPdfSECCompliant = checkForStandard(pdfBoxDoc, "PDF/SEC"); // This might not be effective since PDF/SEC was under development in 2021. checkForStandard(
pdfBoxDoc,
"PDF/B"); // If you want to check for PDF/Broadcast, though this isn't
// an official ISO standard.
boolean isPdfSECCompliant =
checkForStandard(
pdfBoxDoc,
"PDF/SEC"); // This might not be effective since PDF/SEC was under
// development in 2021.
compliancy.put("IsPDF/ACompliant", isPdfACompliant); compliancy.put("IsPDF/ACompliant", isPdfACompliant);
compliancy.put("IsPDF/XCompliant", isPdfXCompliant); compliancy.put("IsPDF/XCompliant", isPdfXCompliant);
@ -304,10 +293,6 @@ public class GetInfoOnPDF {
compliancy.put("IsPDF/BCompliant", isPdfBCompliant); compliancy.put("IsPDF/BCompliant", isPdfBCompliant);
compliancy.put("IsPDF/SECCompliant", isPdfSECCompliant); compliancy.put("IsPDF/SECCompliant", isPdfSECCompliant);
PDOutlineNode root = pdfBoxDoc.getDocumentCatalog().getDocumentOutline(); PDOutlineNode root = pdfBoxDoc.getDocumentCatalog().getDocumentOutline();
ArrayNode bookmarksArray = objectMapper.createArrayNode(); ArrayNode bookmarksArray = objectMapper.createArrayNode();
@ -319,8 +304,6 @@ public class GetInfoOnPDF {
other.set("Bookmarks/Outline/TOC", bookmarksArray); other.set("Bookmarks/Outline/TOC", bookmarksArray);
PDMetadata pdMetadata = pdfBoxDoc.getDocumentCatalog().getMetadata(); PDMetadata pdMetadata = pdfBoxDoc.getDocumentCatalog().getMetadata();
String xmpString = null; String xmpString = null;
@ -341,8 +324,6 @@ public class GetInfoOnPDF {
other.put("XMPMetadata", xmpString); other.put("XMPMetadata", xmpString);
if (pdfBoxDoc.isEncrypted()) { if (pdfBoxDoc.isEncrypted()) {
encryption.put("IsEncrypted", true); encryption.put("IsEncrypted", true);
@ -356,23 +337,22 @@ public class GetInfoOnPDF {
permissionsNode.put("CanAssembleDocument", ap.canAssembleDocument()); permissionsNode.put("CanAssembleDocument", ap.canAssembleDocument());
permissionsNode.put("CanExtractContent", ap.canExtractContent()); permissionsNode.put("CanExtractContent", ap.canExtractContent());
permissionsNode.put("CanExtractForAccessibility", ap.canExtractForAccessibility()); permissionsNode.put(
"CanExtractForAccessibility", ap.canExtractForAccessibility());
permissionsNode.put("CanFillInForm", ap.canFillInForm()); permissionsNode.put("CanFillInForm", ap.canFillInForm());
permissionsNode.put("CanModify", ap.canModify()); permissionsNode.put("CanModify", ap.canModify());
permissionsNode.put("CanModifyAnnotations", ap.canModifyAnnotations()); permissionsNode.put("CanModifyAnnotations", ap.canModifyAnnotations());
permissionsNode.put("CanPrint", ap.canPrint()); permissionsNode.put("CanPrint", ap.canPrint());
permissionsNode.put("CanPrintDegraded", ap.canPrintDegraded()); permissionsNode.put("CanPrintDegraded", ap.canPrintDegraded());
encryption.set("Permissions", permissionsNode); // set the node under "Permissions" encryption.set(
"Permissions", permissionsNode); // set the node under "Permissions"
} }
// Add other encryption-related properties as needed // Add other encryption-related properties as needed
} else { } else {
encryption.put("IsEncrypted", false); encryption.put("IsEncrypted", false);
} }
ObjectNode pageInfoParent = objectMapper.createObjectNode(); ObjectNode pageInfoParent = objectMapper.createObjectNode();
for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) { for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) {
ObjectNode pageInfo = objectMapper.createObjectNode(); ObjectNode pageInfo = objectMapper.createObjectNode();
@ -396,11 +376,11 @@ public class GetInfoOnPDF {
pageInfo.put("Rotation", page.getRotation()); pageInfo.put("Rotation", page.getRotation());
pageInfo.put("Page Orientation", getPageOrientation(width, height)); pageInfo.put("Page Orientation", getPageOrientation(width, height));
// Boxes // Boxes
pageInfo.put("MediaBox", mediaBox.toString()); pageInfo.put("MediaBox", mediaBox.toString());
// Assuming the following boxes are defined for your document; if not, you may get null values. // Assuming the following boxes are defined for your document; if not, you may get
// null values.
PDRectangle cropBox = page.getCropBox(); PDRectangle cropBox = page.getCropBox();
pageInfo.put("CropBox", cropBox == null ? "Undefined" : cropBox.toString()); pageInfo.put("CropBox", cropBox == null ? "Undefined" : cropBox.toString());
@ -416,7 +396,7 @@ public class GetInfoOnPDF {
// Content Extraction // Content Extraction
PDFTextStripper textStripper = new PDFTextStripper(); PDFTextStripper textStripper = new PDFTextStripper();
textStripper.setStartPage(pageNum + 1); textStripper.setStartPage(pageNum + 1);
textStripper.setEndPage(pageNum +1); textStripper.setEndPage(pageNum + 1);
String pageText = textStripper.getText(pdfBoxDoc); String pageText = textStripper.getText(pdfBoxDoc);
pageInfo.put("Text Characters Count", pageText.length()); // pageInfo.put("Text Characters Count", pageText.length()); //
@ -443,15 +423,12 @@ public class GetInfoOnPDF {
annotationsObject.put("ContentsCount", contentsCount); annotationsObject.put("ContentsCount", contentsCount);
pageInfo.set("Annotations", annotationsObject); pageInfo.set("Annotations", annotationsObject);
// Images (simplified) // Images (simplified)
// This part is non-trivial as images can be embedded in multiple ways in a PDF. // This part is non-trivial as images can be embedded in multiple ways in a PDF.
// Here is a basic structure to recognize image XObjects on a page. // Here is a basic structure to recognize image XObjects on a page.
ArrayNode imagesArray = objectMapper.createArrayNode(); ArrayNode imagesArray = objectMapper.createArrayNode();
PDResources resources = page.getResources(); PDResources resources = page.getResources();
for (COSName name : resources.getXObjectNames()) { for (COSName name : resources.getXObjectNames()) {
PDXObject xObject = resources.getXObject(name); PDXObject xObject = resources.getXObject(name);
if (xObject instanceof PDImageXObject) { if (xObject instanceof PDImageXObject) {
@ -460,7 +437,9 @@ public class GetInfoOnPDF {
ObjectNode imageNode = objectMapper.createObjectNode(); ObjectNode imageNode = objectMapper.createObjectNode();
imageNode.put("Width", image.getWidth()); imageNode.put("Width", image.getWidth());
imageNode.put("Height", image.getHeight()); imageNode.put("Height", image.getHeight());
if(image.getMetadata() != null && image.getMetadata().getFile() != null && image.getMetadata().getFile().getFile() != null) { if (image.getMetadata() != null
&& image.getMetadata().getFile() != null
&& image.getMetadata().getFile().getFile() != null) {
imageNode.put("Name", image.getMetadata().getFile().getFile()); imageNode.put("Name", image.getMetadata().getFile().getFile());
} }
if (image.getColorSpace() != null) { if (image.getColorSpace() != null) {
@ -472,7 +451,6 @@ public class GetInfoOnPDF {
} }
pageInfo.set("Images", imagesArray); pageInfo.set("Images", imagesArray);
// Links // Links
ArrayNode linksArray = objectMapper.createArrayNode(); ArrayNode linksArray = objectMapper.createArrayNode();
Set<String> uniqueURIs = new HashSet<>(); // To store unique URIs Set<String> uniqueURIs = new HashSet<>(); // To store unique URIs
@ -496,7 +474,6 @@ public class GetInfoOnPDF {
} }
pageInfo.set("Links", linksArray); pageInfo.set("Links", linksArray);
// Fonts // Fonts
ArrayNode fontsArray = objectMapper.createArrayNode(); ArrayNode fontsArray = objectMapper.createArrayNode();
Map<String, ObjectNode> uniqueFontsMap = new HashMap<>(); Map<String, ObjectNode> uniqueFontsMap = new HashMap<>();
@ -526,11 +503,11 @@ public class GetInfoOnPDF {
fontNode.put("IsNonsymbolic", (flags & 32) != 0); fontNode.put("IsNonsymbolic", (flags & 32) != 0);
fontNode.put("FontFamily", fontDescriptor.getFontFamily()); fontNode.put("FontFamily", fontDescriptor.getFontFamily());
// Font stretch and BBox are not directly available in PDFBox's API, so these are omitted for simplicity // Font stretch and BBox are not directly available in PDFBox's API, so
// these are omitted for simplicity
fontNode.put("FontWeight", fontDescriptor.getFontWeight()); fontNode.put("FontWeight", fontDescriptor.getFontWeight());
} }
// Create a unique key for this font node based on its attributes // Create a unique key for this font node based on its attributes
String uniqueKey = fontNode.toString(); String uniqueKey = fontNode.toString();
@ -552,16 +529,6 @@ public class GetInfoOnPDF {
pageInfo.set("Fonts", fontsArray); pageInfo.set("Fonts", fontsArray);
// Access resources dictionary // Access resources dictionary
ArrayNode colorSpacesArray = objectMapper.createArrayNode(); ArrayNode colorSpacesArray = objectMapper.createArrayNode();
@ -581,9 +548,9 @@ public class GetInfoOnPDF {
} }
pageInfo.set("Color Spaces & ICC Profiles", colorSpacesArray); pageInfo.set("Color Spaces & ICC Profiles", colorSpacesArray);
// Other XObjects // Other XObjects
Map<String, Integer> xObjectCountMap = new HashMap<>(); // To store the count for each type Map<String, Integer> xObjectCountMap =
new HashMap<>(); // To store the count for each type
for (COSName name : resources.getXObjectNames()) { for (COSName name : resources.getXObjectNames()) {
PDXObject xObject = resources.getXObject(name); PDXObject xObject = resources.getXObject(name);
String xObjectType; String xObjectType;
@ -597,7 +564,8 @@ public class GetInfoOnPDF {
} }
// Increment the count for this type in the map // Increment the count for this type in the map
xObjectCountMap.put(xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1); xObjectCountMap.put(
xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1);
} }
// Add the count map to pageInfo (or wherever you want to store it) // Add the count map to pageInfo (or wherever you want to store it)
@ -607,9 +575,6 @@ public class GetInfoOnPDF {
} }
pageInfo.set("XObjectCounts", xObjectCountNode); pageInfo.set("XObjectCounts", xObjectCountNode);
ArrayNode multimediaArray = objectMapper.createArrayNode(); ArrayNode multimediaArray = objectMapper.createArrayNode();
for (PDAnnotation annotation : annotations) { for (PDAnnotation annotation : annotations) {
@ -622,12 +587,9 @@ public class GetInfoOnPDF {
pageInfo.set("Multimedia", multimediaArray); pageInfo.set("Multimedia", multimediaArray);
pageInfoParent.set("Page " + (pageNum + 1), pageInfo);
pageInfoParent.set("Page " + (pageNum+1), pageInfo);
} }
jsonOutput.set("BasicInfo", basicInfo); jsonOutput.set("BasicInfo", basicInfo);
jsonOutput.set("DocumentInfo", docInfoNode); jsonOutput.set("DocumentInfo", docInfoNode);
jsonOutput.set("Compliancy", compliancy); jsonOutput.set("Compliancy", compliancy);
@ -635,14 +597,14 @@ public class GetInfoOnPDF {
jsonOutput.set("Other", other); jsonOutput.set("Other", other);
jsonOutput.set("PerPageInfo", pageInfoParent); jsonOutput.set("PerPageInfo", pageInfoParent);
// Save JSON to file // Save JSON to file
String jsonString = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonOutput); String jsonString =
objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonOutput);
return WebResponseUtils.bytesToWebResponse(
jsonString.getBytes(StandardCharsets.UTF_8),
return WebResponseUtils.bytesToWebResponse(jsonString.getBytes(StandardCharsets.UTF_8), "response.json", MediaType.APPLICATION_JSON); "response.json",
MediaType.APPLICATION_JSON);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
@ -674,6 +636,7 @@ public class GetInfoOnPDF {
return "Square"; return "Square";
} }
} }
public String getPageSize(float width, float height) { public String getPageSize(float width, float height) {
// Define standard page sizes // Define standard page sizes
Map<String, PDRectangle> standardSizes = new HashMap<>(); Map<String, PDRectangle> standardSizes = new HashMap<>();
@ -696,12 +659,13 @@ public class GetInfoOnPDF {
return "Custom"; return "Custom";
} }
private boolean isCloseToSize(float width, float height, float standardWidth, float standardHeight) { private boolean isCloseToSize(
float width, float height, float standardWidth, float standardHeight) {
float tolerance = 1.0f; // You can adjust the tolerance as needed float tolerance = 1.0f; // You can adjust the tolerance as needed
return Math.abs(width - standardWidth) <= tolerance && Math.abs(height - standardHeight) <= tolerance; return Math.abs(width - standardWidth) <= tolerance
&& Math.abs(height - standardHeight) <= tolerance;
} }
public ObjectNode getDimensionInfo(ObjectNode dimensionInfo, float width, float height) { public ObjectNode getDimensionInfo(ObjectNode dimensionInfo, float width, float height) {
float ppi = 72; // Points Per Inch float ppi = 72; // Points Per Inch
@ -720,9 +684,7 @@ public class GetInfoOnPDF {
return dimensionInfo; return dimensionInfo;
} }
public static boolean checkForStandard(PDDocument document, String standardKeyword) {
public static boolean checkForStandard(PDDocument document, String standardKeyword) {
// Check XMP Metadata // Check XMP Metadata
try { try {
PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata(); PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata();
@ -739,13 +701,15 @@ public static boolean checkForStandard(PDDocument document, String standardKeywo
return true; 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(); e.printStackTrace();
} }
return false; return false;
} }
public ArrayNode exploreStructureTree(List<Object> nodes) { public ArrayNode exploreStructureTree(List<Object> nodes) {
ArrayNode elementsArray = objectMapper.createArrayNode(); ArrayNode elementsArray = objectMapper.createArrayNode();
@ -773,7 +737,6 @@ public static boolean checkForStandard(PDDocument document, String standardKeywo
return elementsArray; return elementsArray;
} }
public String getContent(PDStructureElement structureElement) { public String getContent(PDStructureElement structureElement) {
StringBuilder contentBuilder = new StringBuilder(); StringBuilder contentBuilder = new StringBuilder();
@ -791,7 +754,6 @@ public static boolean checkForStandard(PDDocument document, String standardKeywo
return contentBuilder.toString(); return contentBuilder.toString();
} }
private String formatDate(Calendar calendar) { private String formatDate(Calendar calendar) {
if (calendar != null) { if (calendar != null) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

View File

@ -16,9 +16,11 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.AddPasswordRequest; import stirling.software.SPDF.model.api.security.AddPasswordRequest;
import stirling.software.SPDF.model.api.security.PDFPasswordRequest; import stirling.software.SPDF.model.api.security.PDFPasswordRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/security") @RequestMapping("/api/v1/security")
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
@ -26,29 +28,31 @@ public class PasswordController {
private static final Logger logger = LoggerFactory.getLogger(PasswordController.class); private static final Logger logger = LoggerFactory.getLogger(PasswordController.class);
@PostMapping(consumes = "multipart/form-data", value = "/remove-password") @PostMapping(consumes = "multipart/form-data", value = "/remove-password")
@Operation( @Operation(
summary = "Remove password from a PDF file", summary = "Remove password from a PDF file",
description = "This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO" description =
) "This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request) throws IOException { public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request)
throws IOException {
MultipartFile fileInput = request.getFileInput(); MultipartFile fileInput = request.getFileInput();
String password = request.getPassword(); String password = request.getPassword();
PDDocument document = PDDocument.load(fileInput.getBytes(), password); PDDocument document = PDDocument.load(fileInput.getBytes(), password);
document.setAllSecurityToBeRemoved(true); document.setAllSecurityToBeRemoved(true);
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_password_removed.pdf"); return WebResponseUtils.pdfDocToWebResponse(
document,
fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_password_removed.pdf");
} }
@PostMapping(consumes = "multipart/form-data", value = "/add-password") @PostMapping(consumes = "multipart/form-data", value = "/add-password")
@Operation( @Operation(
summary = "Add password to a PDF file", summary = "Add password to a PDF file",
description = "This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF" description =
) "This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF")
public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request) throws IOException { public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request)
throws IOException {
MultipartFile fileInput = request.getFileInput(); MultipartFile fileInput = request.getFileInput();
String ownerPassword = request.getOwnerPassword(); String ownerPassword = request.getOwnerPassword();
String password = request.getPassword(); String password = request.getPassword();
@ -74,16 +78,19 @@ public class PasswordController {
ap.setCanPrintFaithful(!canPrintFaithful); ap.setCanPrintFaithful(!canPrintFaithful);
StandardProtectionPolicy spp = new StandardProtectionPolicy(ownerPassword, password, ap); StandardProtectionPolicy spp = new StandardProtectionPolicy(ownerPassword, password, ap);
if(!"".equals(ownerPassword) || !"".equals(password)) { if (!"".equals(ownerPassword) || !"".equals(password)) {
spp.setEncryptionKeyLength(keyLength); spp.setEncryptionKeyLength(keyLength);
} }
spp.setPermissions(ap); spp.setPermissions(ap);
document.protect(spp); document.protect(spp);
if("".equals(ownerPassword) && "".equals(password)) if ("".equals(ownerPassword) && "".equals(password))
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_permissions.pdf"); return WebResponseUtils.pdfDocToWebResponse(
return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf"); document,
fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_permissions.pdf");
return WebResponseUtils.pdfDocToWebResponse(
document,
fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf");
} }
} }

View File

@ -26,10 +26,12 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.PDFText; import stirling.software.SPDF.model.PDFText;
import stirling.software.SPDF.model.api.security.RedactPdfRequest; import stirling.software.SPDF.model.api.security.RedactPdfRequest;
import stirling.software.SPDF.pdf.TextFinder; import stirling.software.SPDF.pdf.TextFinder;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/security") @RequestMapping("/api/v1/security")
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
@ -37,11 +39,13 @@ public class RedactController {
private static final Logger logger = LoggerFactory.getLogger(RedactController.class); private static final Logger logger = LoggerFactory.getLogger(RedactController.class);
@PostMapping(value = "/auto-redact", consumes = "multipart/form-data") @PostMapping(value = "/auto-redact", consumes = "multipart/form-data")
@Operation(summary = "Redacts listOfText in a PDF document", @Operation(
description = "This operation takes an input PDF file and redacts the provided listOfText. Input:PDF, Output:PDF, Type:SISO") summary = "Redacts listOfText in a PDF document",
public ResponseEntity<byte[]> redactPdf(@ModelAttribute RedactPdfRequest request) throws Exception { description =
"This operation takes an input PDF file and redacts the provided listOfText. Input:PDF, Output:PDF, Type:SISO")
public ResponseEntity<byte[]> redactPdf(@ModelAttribute RedactPdfRequest request)
throws Exception {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
String listOfTextString = request.getListOfText(); String listOfTextString = request.getListOfText();
boolean useRegex = request.isUseRegex(); boolean useRegex = request.isUseRegex();
@ -66,18 +70,14 @@ public class RedactController {
redactColor = Color.BLACK; redactColor = Color.BLACK;
} }
for (String text : listOfText) { for (String text : listOfText) {
text = text.trim(); text = text.trim();
System.out.println(text); System.out.println(text);
TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool); TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool);
List<PDFText> foundTexts = textFinder.getTextLocations(document); List<PDFText> foundTexts = textFinder.getTextLocations(document);
redactFoundText(document, foundTexts, customPadding,redactColor); redactFoundText(document, foundTexts, customPadding, redactColor);
} }
if (convertPDFToImage) { if (convertPDFToImage) {
PDDocument imageDocument = new PDDocument(); PDDocument imageDocument = new PDDocument();
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);
@ -99,25 +99,31 @@ public class RedactController {
document.close(); document.close();
byte[] pdfContent = baos.toByteArray(); byte[] pdfContent = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(pdfContent, return WebResponseUtils.bytesToWebResponse(
pdfContent,
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_redacted.pdf"); file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_redacted.pdf");
} }
private void redactFoundText(
private void redactFoundText(PDDocument document, List<PDFText> blocks, float customPadding, Color redactColor) throws IOException { PDDocument document, List<PDFText> blocks, float customPadding, Color redactColor)
throws IOException {
var allPages = document.getDocumentCatalog().getPages(); var allPages = document.getDocumentCatalog().getPages();
for (PDFText block : blocks) { for (PDFText block : blocks) {
var page = allPages.get(block.getPageIndex()); var page = allPages.get(block.getPageIndex());
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true, true); PDPageContentStream contentStream =
new PDPageContentStream(
document, page, PDPageContentStream.AppendMode.APPEND, true, true);
contentStream.setNonStrokingColor(redactColor); contentStream.setNonStrokingColor(redactColor);
float padding = (block.getY2() - block.getY1()) * 0.3f + customPadding; float padding = (block.getY2() - block.getY1()) * 0.3f + customPadding;
PDRectangle pageBox = page.getBBox(); PDRectangle pageBox = page.getBBox();
contentStream.addRect(block.getX1(), pageBox.getHeight() - block.getY1() - padding, block.getX2() - block.getX1(), block.getY2() - block.getY1() + 2 * padding); contentStream.addRect(
block.getX1(),
pageBox.getHeight() - block.getY1() - padding,
block.getX2() - block.getX1(),
block.getY2() - block.getY1() + 2 * padding);
contentStream.fill(); contentStream.fill();
contentStream.close(); contentStream.close();
} }
} }
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.api.security; package stirling.software.SPDF.controller.api.security;
import java.io.IOException; import java.io.IOException;
import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSDictionary;
@ -28,6 +29,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.SanitizePdfRequest; import stirling.software.SPDF.model.api.security.SanitizePdfRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -37,9 +39,12 @@ import stirling.software.SPDF.utils.WebResponseUtils;
public class SanitizeController { public class SanitizeController {
@PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf") @PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf")
@Operation(summary = "Sanitize a PDF file", @Operation(
description = "This endpoint processes a PDF file and removes specific elements based on the provided options. Input:PDF Output:PDF Type:SISO") summary = "Sanitize a PDF file",
public ResponseEntity<byte[]> sanitizePDF(@ModelAttribute SanitizePdfRequest request) throws IOException { description =
"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(); MultipartFile inputFile = request.getFileInput();
boolean removeJavaScript = request.isRemoveJavaScript(); boolean removeJavaScript = request.isRemoveJavaScript();
boolean removeEmbeddedFiles = request.isRemoveEmbeddedFiles(); boolean removeEmbeddedFiles = request.isRemoveEmbeddedFiles();
@ -68,19 +73,25 @@ public class SanitizeController {
sanitizeFonts(document); 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 { private void sanitizeJavaScript(PDDocument document) throws IOException {
// Get the root dictionary (catalog) of the PDF // Get the root dictionary (catalog) of the PDF
PDDocumentCatalog catalog = document.getDocumentCatalog(); PDDocumentCatalog catalog = document.getDocumentCatalog();
// Get the Names dictionary // Get the Names dictionary
COSDictionary namesDict = (COSDictionary) catalog.getCOSObject().getDictionaryObject(COSName.NAMES); COSDictionary namesDict =
(COSDictionary) catalog.getCOSObject().getDictionaryObject(COSName.NAMES);
if (namesDict != null) { if (namesDict != null) {
// Get the JavaScript dictionary // Get the JavaScript dictionary
COSDictionary javaScriptDict = (COSDictionary) namesDict.getDictionaryObject(COSName.getPDFName("JavaScript")); COSDictionary javaScriptDict =
(COSDictionary) namesDict.getDictionaryObject(COSName.getPDFName("JavaScript"));
if (javaScriptDict != null) { if (javaScriptDict != null) {
// Remove the JavaScript dictionary // Remove the JavaScript dictionary
@ -102,7 +113,7 @@ public class SanitizeController {
if (acroForm != null) { if (acroForm != null) {
for (PDField field : acroForm.getFields()) { for (PDField field : acroForm.getFields()) {
PDFormFieldAdditionalActions actions = field.getActions(); PDFormFieldAdditionalActions actions = field.getActions();
if(actions != null) { if (actions != null) {
if (actions.getC() instanceof PDActionJavaScript) { if (actions.getC() instanceof PDActionJavaScript) {
actions.setC(null); actions.setC(null);
} }
@ -121,9 +132,6 @@ public class SanitizeController {
} }
} }
private void sanitizeEmbeddedFiles(PDDocument document) { private void sanitizeEmbeddedFiles(PDDocument document) {
PDPageTree allPages = document.getPages(); PDPageTree allPages = document.getPages();
@ -135,7 +143,6 @@ public class SanitizeController {
} }
} }
private void sanitizeMetadata(PDDocument document) { private void sanitizeMetadata(PDDocument document) {
PDMetadata metadata = document.getDocumentCatalog().getMetadata(); PDMetadata metadata = document.getDocumentCatalog().getMetadata();
if (metadata != null) { if (metadata != null) {
@ -143,8 +150,6 @@ public class SanitizeController {
} }
} }
private void sanitizeLinks(PDDocument document) throws IOException { private void sanitizeLinks(PDDocument document) throws IOException {
for (PDPage page : document.getPages()) { for (PDPage page : document.getPages()) {
for (PDAnnotation annotation : page.getAnnotations()) { for (PDAnnotation annotation : page.getAnnotations()) {
@ -163,5 +168,4 @@ public class SanitizeController {
page.getResources().getCOSObject().removeItem(COSName.getPDFName("Font")); page.getResources().getCOSObject().removeItem(COSName.getPDFName("Font"));
} }
} }
} }

View File

@ -30,6 +30,7 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.security.AddWatermarkRequest; import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -39,8 +40,12 @@ import stirling.software.SPDF.utils.WebResponseUtils;
public class WatermarkController { public class WatermarkController {
@PostMapping(consumes = "multipart/form-data", value = "/add-watermark") @PostMapping(consumes = "multipart/form-data", value = "/add-watermark")
@Operation(summary = "Add watermark to a PDF file", description = "This endpoint adds a watermark to a given PDF file. Users can specify the watermark type (text or image), rotation, opacity, width spacer, and height spacer. Input:PDF Output:PDF Type:SISO") @Operation(
public ResponseEntity<byte[]> addWatermark(@ModelAttribute AddWatermarkRequest request) throws IOException, Exception { summary = "Add watermark to a PDF file",
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(); MultipartFile pdfFile = request.getFileInput();
String watermarkType = request.getWatermarkType(); String watermarkType = request.getWatermarkType();
String watermarkText = request.getWatermarkText(); String watermarkText = request.getWatermarkText();
@ -59,8 +64,9 @@ public class WatermarkController {
for (PDPage page : document.getPages()) { for (PDPage page : document.getPages()) {
// Get the page's content stream // Get the page's content stream
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream contentStream =
PDPageContentStream.AppendMode.APPEND, true); new PDPageContentStream(
document, page, PDPageContentStream.AppendMode.APPEND, true);
// Set transparency // Set transparency
PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState(); PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
@ -68,10 +74,25 @@ public class WatermarkController {
contentStream.setGraphicsStateParameters(graphicsState); contentStream.setGraphicsStateParameters(graphicsState);
if (watermarkType.equalsIgnoreCase("text")) { if (watermarkType.equalsIgnoreCase("text")) {
addTextWatermark(contentStream, watermarkText, document, page, rotation, widthSpacer, heightSpacer, addTextWatermark(
fontSize, alphabet); contentStream,
watermarkText,
document,
page,
rotation,
widthSpacer,
heightSpacer,
fontSize,
alphabet);
} else if (watermarkType.equalsIgnoreCase("image")) { } else if (watermarkType.equalsIgnoreCase("image")) {
addImageWatermark(contentStream, watermarkImage, document, page, rotation, widthSpacer, heightSpacer, addImageWatermark(
contentStream,
watermarkImage,
document,
page,
rotation,
widthSpacer,
heightSpacer,
fontSize); fontSize);
} }
@ -79,12 +100,22 @@ public class WatermarkController {
contentStream.close(); contentStream.close();
} }
return WebResponseUtils.pdfDocToWebResponse(document, return WebResponseUtils.pdfDocToWebResponse(
document,
pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf"); pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf");
} }
private void addTextWatermark(PDPageContentStream contentStream, String watermarkText, PDDocument document, private void addTextWatermark(
PDPage page, float rotation, int widthSpacer, int heightSpacer, float fontSize, String alphabet) throws IOException { PDPageContentStream contentStream,
String watermarkText,
PDDocument document,
PDPage page,
float rotation,
int widthSpacer,
int heightSpacer,
float fontSize,
String alphabet)
throws IOException {
String resourceDir = ""; String resourceDir = "";
PDFont font = PDType1Font.HELVETICA_BOLD; PDFont font = PDType1Font.HELVETICA_BOLD;
switch (alphabet) { switch (alphabet) {
@ -106,12 +137,12 @@ public class WatermarkController {
break; break;
} }
if (!resourceDir.equals("")) {
if(!resourceDir.equals("")) {
ClassPathResource classPathResource = new ClassPathResource(resourceDir); ClassPathResource classPathResource = new ClassPathResource(resourceDir);
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf(".")); String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
File tempFile = File.createTempFile("NotoSansFont", fileExtension); File tempFile = File.createTempFile("NotoSansFont", fileExtension);
try (InputStream is = classPathResource.getInputStream(); FileOutputStream os = new FileOutputStream(tempFile)) { try (InputStream is = classPathResource.getInputStream();
FileOutputStream os = new FileOutputStream(tempFile)) {
IOUtils.copy(is, os); IOUtils.copy(is, os);
} }
@ -134,58 +165,72 @@ public class WatermarkController {
for (int i = 0; i < watermarkRows; i++) { for (int i = 0; i < watermarkRows; i++) {
for (int j = 0; j < watermarkCols; j++) { for (int j = 0; j < watermarkCols; j++) {
contentStream.beginText(); contentStream.beginText();
contentStream.setTextMatrix(Matrix.getRotateInstance((float) Math.toRadians(rotation), contentStream.setTextMatrix(
j * watermarkWidth, i * watermarkHeight)); Matrix.getRotateInstance(
(float) Math.toRadians(rotation),
j * watermarkWidth,
i * watermarkHeight));
contentStream.showText(watermarkText); contentStream.showText(watermarkText);
contentStream.endText(); contentStream.endText();
} }
} }
} }
private void addImageWatermark(PDPageContentStream contentStream, MultipartFile watermarkImage, PDDocument document, PDPage page, float rotation, private void addImageWatermark(
int widthSpacer, int heightSpacer, float fontSize) throws IOException { PDPageContentStream contentStream,
MultipartFile watermarkImage,
PDDocument document,
PDPage page,
float rotation,
int widthSpacer,
int heightSpacer,
float fontSize)
throws IOException {
// Load the watermark image // Load the watermark image
BufferedImage image = ImageIO.read(watermarkImage.getInputStream()); BufferedImage image = ImageIO.read(watermarkImage.getInputStream());
// Compute width based on original aspect ratio // Compute width based on original aspect ratio
float aspectRatio = (float) image.getWidth() / (float) image.getHeight(); float aspectRatio = (float) image.getWidth() / (float) image.getHeight();
// Desired physical height (in PDF points) // Desired physical height (in PDF points)
float desiredPhysicalHeight = fontSize ; float desiredPhysicalHeight = fontSize;
// Desired physical width based on the aspect ratio // Desired physical width based on the aspect ratio
float desiredPhysicalWidth = desiredPhysicalHeight * aspectRatio; float desiredPhysicalWidth = desiredPhysicalHeight * aspectRatio;
// Convert the BufferedImage to PDImageXObject // Convert the BufferedImage to PDImageXObject
PDImageXObject xobject = LosslessFactory.createFromImage(document, image); PDImageXObject xobject = LosslessFactory.createFromImage(document, image);
// Calculate the number of rows and columns for watermarks // Calculate the number of rows and columns for watermarks
float pageWidth = page.getMediaBox().getWidth(); float pageWidth = page.getMediaBox().getWidth();
float pageHeight = page.getMediaBox().getHeight(); float pageHeight = page.getMediaBox().getHeight();
int watermarkRows = (int) ((pageHeight + heightSpacer) / (desiredPhysicalHeight + heightSpacer)); int watermarkRows =
int watermarkCols = (int) ((pageWidth + widthSpacer) / (desiredPhysicalWidth + widthSpacer)); (int) ((pageHeight + heightSpacer) / (desiredPhysicalHeight + heightSpacer));
int watermarkCols =
(int) ((pageWidth + widthSpacer) / (desiredPhysicalWidth + widthSpacer));
for (int i = 0; i < watermarkRows; i++) { for (int i = 0; i < watermarkRows; i++) {
for (int j = 0; j < watermarkCols; j++) { for (int j = 0; j < watermarkCols; j++) {
float x = j * (desiredPhysicalWidth + widthSpacer); float x = j * (desiredPhysicalWidth + widthSpacer);
float y = i * (desiredPhysicalHeight + heightSpacer); float y = i * (desiredPhysicalHeight + heightSpacer);
// Save the graphics state // Save the graphics state
contentStream.saveGraphicsState(); contentStream.saveGraphicsState();
// Create rotation matrix and rotate // Create rotation matrix and rotate
contentStream.transform(Matrix.getTranslateInstance(x + desiredPhysicalWidth / 2, y + desiredPhysicalHeight / 2)); contentStream.transform(
contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0)); Matrix.getTranslateInstance(
contentStream.transform(Matrix.getTranslateInstance(-desiredPhysicalWidth / 2, -desiredPhysicalHeight / 2)); x + desiredPhysicalWidth / 2, y + desiredPhysicalHeight / 2));
contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0));
// Draw the image and restore the graphics state contentStream.transform(
contentStream.drawImage(xobject, 0, 0, desiredPhysicalWidth, desiredPhysicalHeight); Matrix.getTranslateInstance(
contentStream.restoreGraphicsState(); -desiredPhysicalWidth / 2, -desiredPhysicalHeight / 2));
}
}
// Draw the image and restore the graphics state
contentStream.drawImage(xobject, 0, 0, desiredPhysicalWidth, desiredPhysicalHeight);
contentStream.restoreGraphicsState();
}
}
} }
} }

View File

@ -24,91 +24,79 @@ import org.apache.pdfbox.text.PDFTextStripperByArea;
import org.apache.pdfbox.text.TextPosition; 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. * <p>Works best when headers are included in the detected region, to ensure representative text in
* Works by making a first pass of the page to group all nearby text items * every column.
* together, and then inferring a 2D grid from these regions. Each table cell
* is then extracted using a PDFTextStripperByArea object.
* *
* Works best when * <p>Based upon DrawPrintTextLocations PDFBox example
* headers are included in the detected region, to ensure representative text
* in every column.
*
* Based upon DrawPrintTextLocations PDFBox example
* (https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/org/apache/pdfbox/examples/util/DrawPrintTextLocations.java) * (https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/org/apache/pdfbox/examples/util/DrawPrintTextLocations.java)
* *
* @author Beldaz * @author Beldaz
*/ */
public class PDFTableStripper extends PDFTextStripper public class PDFTableStripper extends PDFTextStripper {
{
/** /**
* This will print the documents data, for each table cell. * This will print the documents data, for each table cell.
* *
* @param args The command line arguments. * @param args The command line arguments.
*
* @throws IOException If there is an error parsing the document. * @throws IOException If there is an error parsing the document.
*/ */
/* /*
* Used in methods derived from DrawPrintTextLocations * Used in methods derived from DrawPrintTextLocations
*/ */
private AffineTransform flipAT; private AffineTransform flipAT;
private AffineTransform rotateAT; private AffineTransform rotateAT;
/** /** Regions updated by calls to writeString */
* Regions updated by calls to writeString
*/
private Set<Rectangle2D> boxes; private Set<Rectangle2D> boxes;
// Border to allow when finding intersections // Border to allow when finding intersections
private double dx = 1.0; // This value works for me, feel free to tweak (or add setter) private double dx = 1.0; // This value works for me, feel free to tweak (or add setter)
private double dy = 0.000; // Rows of text tend to overlap, so need to extend private double dy = 0.000; // Rows of text tend to overlap, so need to extend
/** /** Region in which to find table (otherwise whole page) */
* Region in which to find table (otherwise whole page)
*/
private Rectangle2D regionArea; private Rectangle2D regionArea;
/** /** Number of rows in inferred table */
* Number of rows in inferred table private int nRows = 0;
*/
private int nRows=0;
/** /** Number of columns in inferred table */
* Number of columns in inferred table private int nCols = 0;
*/
private int nCols=0;
/** /** This is the object that does the text extraction */
* This is the object that does the text extraction
*/
private PDFTextStripperByArea regionStripper; private PDFTextStripperByArea regionStripper;
/** /**
* 1D intervals - used for calculateTableRegions() * 1D intervals - used for calculateTableRegions()
* @author Beldaz
* *
* @author Beldaz
*/ */
public static class Interval { public static class Interval {
double start; double start;
double end; double end;
public Interval(double start, double end) { public Interval(double start, double end) {
this.start=start; this.end = end; this.start = start;
this.end = end;
} }
public void add(Interval col) { public void add(Interval col) {
if(col.start<start) if (col.start < start) start = col.start;
start = col.start; if (col.end > end) end = col.end;
if(col.end>end)
end = col.end;
} }
public static void addTo(Interval x, LinkedList<Interval> columns) { public static void addTo(Interval x, LinkedList<Interval> columns) {
int p = 0; int p = 0;
Iterator<Interval> it = columns.iterator(); Iterator<Interval> it = columns.iterator();
// Find where x should go // Find where x should go
while(it.hasNext()) { while (it.hasNext()) {
Interval col = it.next(); Interval col = it.next();
if(x.end>=col.start) { if (x.end >= col.start) {
if(x.start<=col.end) { // overlaps if (x.start <= col.end) { // overlaps
x.add(col); x.add(col);
it.remove(); it.remove();
} }
@ -116,30 +104,26 @@ public class PDFTableStripper extends PDFTextStripper
} }
++p; ++p;
} }
while(it.hasNext()) { while (it.hasNext()) {
Interval col = it.next(); Interval col = it.next();
if(x.start>col.end) if (x.start > col.end) break;
break;
x.add(col); x.add(col);
it.remove(); it.remove();
} }
columns.add(p, x); columns.add(p, x);
} }
} }
/** /**
* Instantiate a new PDFTableStripper object. * Instantiate a new PDFTableStripper object.
* *
* @param document * @param document
* @throws IOException If there is an error loading the properties. * @throws IOException If there is an error loading the properties.
*/ */
public PDFTableStripper() throws IOException public PDFTableStripper() throws IOException {
{
super.setShouldSeparateByBeads(false); super.setShouldSeparateByBeads(false);
regionStripper = new PDFTextStripperByArea(); regionStripper = new PDFTextStripperByArea();
regionStripper.setSortByPosition( true ); regionStripper.setSortByPosition(true);
} }
/** /**
@ -147,18 +131,15 @@ public class PDFTableStripper extends PDFTextStripper
* *
* @param rect The rectangle area to retrieve the text from. * @param rect The rectangle area to retrieve the text from.
*/ */
public void setRegion(Rectangle2D rect ) public void setRegion(Rectangle2D rect) {
{
regionArea = rect; regionArea = rect;
} }
public int getRows() public int getRows() {
{
return nRows; return nRows;
} }
public int getColumns() public int getColumns() {
{
return nCols; return nCols;
} }
@ -167,13 +148,11 @@ public class PDFTableStripper extends PDFTextStripper
* *
* @return The text that was identified in that region. * @return The text that was identified in that region.
*/ */
public String getText(int row, int col) public String getText(int row, int col) {
{ return regionStripper.getTextForRegion("el" + col + "x" + row);
return regionStripper.getTextForRegion("el"+col+"x"+row);
} }
public void extractTable(PDPage pdPage) throws IOException public void extractTable(PDPage pdPage) throws IOException {
{
setStartPage(getCurrentPageNo()); setStartPage(getCurrentPageNo());
setEndPage(getCurrentPageNo()); setEndPage(getCurrentPageNo());
@ -186,11 +165,9 @@ public class PDFTableStripper extends PDFTextStripper
// page may be rotated // page may be rotated
rotateAT = new AffineTransform(); rotateAT = new AffineTransform();
int rotation = pdPage.getRotation(); int rotation = pdPage.getRotation();
if (rotation != 0) if (rotation != 0) {
{
PDRectangle mediaBox = pdPage.getMediaBox(); PDRectangle mediaBox = pdPage.getMediaBox();
switch (rotation) switch (rotation) {
{
case 90: case 90:
rotateAT.translate(mediaBox.getHeight(), 0); rotateAT.translate(mediaBox.getHeight(), 0);
break; break;
@ -213,11 +190,12 @@ public class PDFTableStripper extends PDFTextStripper
Rectangle2D[][] regions = calculateTableRegions(); Rectangle2D[][] regions = calculateTableRegions();
// System.err.println("Drawing " + nCols + "x" + nRows + "="+ nRows*nCols + " regions"); // System.err.println("Drawing " + nCols + "x" + nRows + "="+ nRows*nCols + "
for(int i=0; i<nCols; ++i) { // regions");
for(int j=0; j<nRows; ++j) { for (int i = 0; i < nCols; ++i) {
for (int j = 0; j < nRows; ++j) {
final Rectangle2D region = regions[i][j]; final Rectangle2D region = regions[i][j];
regionStripper.addRegion("el"+i+"x"+j, region); regionStripper.addRegion("el" + i + "x" + j, region);
} }
} }
@ -227,8 +205,8 @@ public class PDFTableStripper extends PDFTextStripper
/** /**
* Infer a rectangular grid of regions from the boxes field. * Infer a rectangular grid of regions from the boxes field.
* *
* @return 2D array of table regions (as Rectangle2D objects). Note that * @return 2D array of table regions (as Rectangle2D objects). Note that some of these regions
* some of these regions may have no content. * may have no content.
*/ */
private Rectangle2D[][] calculateTableRegions() { private Rectangle2D[][] calculateTableRegions() {
@ -238,7 +216,7 @@ public class PDFTableStripper extends PDFTextStripper
LinkedList<Interval> columns = new LinkedList<Interval>(); LinkedList<Interval> columns = new LinkedList<Interval>();
LinkedList<Interval> rows = new LinkedList<Interval>(); LinkedList<Interval> rows = new LinkedList<Interval>();
for(Rectangle2D box: boxes) { for (Rectangle2D box : boxes) {
Interval x = new Interval(box.getMinX(), box.getMaxX()); Interval x = new Interval(box.getMinX(), box.getMaxX());
Interval y = new Interval(box.getMinY(), box.getMaxY()); Interval y = new Interval(box.getMinY(), box.getMaxY());
@ -249,12 +227,17 @@ public class PDFTableStripper extends PDFTextStripper
nRows = rows.size(); nRows = rows.size();
nCols = columns.size(); nCols = columns.size();
Rectangle2D[][] regions = new Rectangle2D[nCols][nRows]; Rectangle2D[][] regions = new Rectangle2D[nCols][nRows];
int i=0; int i = 0;
// Label regions from top left, rather than the transformed orientation // Label regions from top left, rather than the transformed orientation
for(Interval column: columns) { for (Interval column : columns) {
int j=0; int j = 0;
for(Interval row: rows) { for (Interval row : rows) {
regions[nCols-i-1][nRows-j-1] = new Rectangle2D.Double(column.start, row.start, column.end - column.start, row.end - row.start); regions[nCols - i - 1][nRows - j - 1] =
new Rectangle2D.Double(
column.start,
row.start,
column.end - column.start,
row.end - row.start);
++j; ++j;
} }
++i; ++i;
@ -264,18 +247,15 @@ public class PDFTableStripper extends PDFTextStripper
} }
/** /**
* Register each character's bounding box, updating boxes field to maintain * Register each character's bounding box, updating boxes field to maintain a list of all
* a list of all distinct groups of characters. * distinct groups of characters.
* *
* Overrides the default functionality of PDFTextStripper. * <p>Overrides the default functionality of PDFTextStripper. Most of this is taken from
* Most of this is taken from DrawPrintTextLocations.java, with extra steps * DrawPrintTextLocations.java, with extra steps at end of main loop
* at end of main loop
*/ */
@Override @Override
protected void writeString(String string, List<TextPosition> textPositions) throws IOException protected void writeString(String string, List<TextPosition> textPositions) throws IOException {
{ for (TextPosition text : textPositions) {
for (TextPosition text : textPositions)
{
// glyph space -> user space // glyph space -> user space
// note: text.getTextMatrix() is *not* the Text Matrix, it's the Text Rendering Matrix // note: text.getTextMatrix() is *not* the Text Matrix, it's the Text Rendering Matrix
AffineTransform at = text.getTextMatrix().createAffineTransform(); AffineTransform at = text.getTextMatrix().createAffineTransform();
@ -283,37 +263,35 @@ public class PDFTableStripper extends PDFTextStripper
BoundingBox bbox = font.getBoundingBox(); BoundingBox bbox = font.getBoundingBox();
// advance width, bbox height (glyph space) // advance width, bbox height (glyph space)
float xadvance = font.getWidth(text.getCharacterCodes()[0]); // todo: should iterate all chars float xadvance =
Rectangle2D.Float rect = new Rectangle2D.Float(0, bbox.getLowerLeftY(), xadvance, bbox.getHeight()); font.getWidth(text.getCharacterCodes()[0]); // todo: should iterate all chars
Rectangle2D.Float rect =
new Rectangle2D.Float(0, bbox.getLowerLeftY(), xadvance, bbox.getHeight());
if (font instanceof PDType3Font) if (font instanceof PDType3Font) {
{
// bbox and font matrix are unscaled // bbox and font matrix are unscaled
at.concatenate(font.getFontMatrix().createAffineTransform()); at.concatenate(font.getFontMatrix().createAffineTransform());
} } else {
else
{
// bbox and font matrix are already scaled to 1000 // bbox and font matrix are already scaled to 1000
at.scale(1/1000f, 1/1000f); at.scale(1 / 1000f, 1 / 1000f);
} }
Shape s = at.createTransformedShape(rect); Shape s = at.createTransformedShape(rect);
s = flipAT.createTransformedShape(s); s = flipAT.createTransformedShape(s);
s = rotateAT.createTransformedShape(s); s = rotateAT.createTransformedShape(s);
// //
// Merge character's bounding box with boxes field // Merge character's bounding box with boxes field
// //
Rectangle2D bounds = s.getBounds2D(); Rectangle2D bounds = s.getBounds2D();
// Pad sides to detect almost touching boxes // Pad sides to detect almost touching boxes
Rectangle2D hitbox = bounds.getBounds2D(); Rectangle2D hitbox = bounds.getBounds2D();
hitbox.add(bounds.getMinX() - dx , bounds.getMinY() - dy); hitbox.add(bounds.getMinX() - dx, bounds.getMinY() - dy);
hitbox.add(bounds.getMaxX() + dx , bounds.getMaxY() + dy); hitbox.add(bounds.getMaxX() + dx, bounds.getMaxY() + dy);
// Find all overlapping boxes // Find all overlapping boxes
List<Rectangle2D> intersectList = new ArrayList<Rectangle2D>(); List<Rectangle2D> intersectList = new ArrayList<Rectangle2D>();
for(Rectangle2D box: boxes) { for (Rectangle2D box : boxes) {
if(box.intersects(hitbox)) { if (box.intersects(hitbox)) {
intersectList.add(box); intersectList.add(box);
} }
} }
@ -321,38 +299,30 @@ public class PDFTableStripper extends PDFTextStripper
// Combine all touching boxes and update // Combine all touching boxes and update
// (NOTE: Potentially this could leave some overlapping boxes un-merged, // (NOTE: Potentially this could leave some overlapping boxes un-merged,
// but it's sufficient for now and get's fixed up in calculateTableRegions) // but it's sufficient for now and get's fixed up in calculateTableRegions)
for(Rectangle2D box: intersectList) { for (Rectangle2D box : intersectList) {
bounds.add(box); bounds.add(box);
boxes.remove(box); boxes.remove(box);
} }
boxes.add(bounds); boxes.add(bounds);
} }
} }
/** /**
* This method does nothing in this derived class, because beads and regions are incompatible. Beads are * This method does nothing in this derived class, because beads and regions are incompatible.
* ignored when stripping by area. * Beads are ignored when stripping by area.
* *
* @param aShouldSeparateByBeads The new grouping of beads. * @param aShouldSeparateByBeads The new grouping of beads.
*/ */
@Override @Override
public final void setShouldSeparateByBeads(boolean aShouldSeparateByBeads) public final void setShouldSeparateByBeads(boolean aShouldSeparateByBeads) {}
{
}
/** /** Adapted from PDFTextStripperByArea {@inheritDoc} */
* Adapted from PDFTextStripperByArea
* {@inheritDoc}
*/
@Override @Override
protected void processTextPosition( TextPosition text ) protected void processTextPosition(TextPosition text) {
{ if (regionArea != null && !regionArea.contains(text.getX(), text.getY())) {
if(regionArea!=null && !regionArea.contains( text.getX(), text.getY() ) ) {
// skip character // skip character
} else { } else {
super.processTextPosition( text ); super.processTextPosition(text);
} }
} }
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.web; package stirling.software.SPDF.controller.web;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -15,16 +16,17 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import stirling.software.SPDF.model.Authority; import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository; import stirling.software.SPDF.repository.UserRepository;
@Controller @Controller
@Tag(name = "Account Security", description = "Account Security APIs") @Tag(name = "Account Security", description = "Account Security APIs")
public class AccountWebController { public class AccountWebController {
@GetMapping("/login") @GetMapping("/login")
public String login(HttpServletRequest request, Model model, Authentication authentication) { public String login(HttpServletRequest request, Model model, Authentication authentication) {
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
@ -42,19 +44,19 @@ public class AccountWebController {
return "login"; return "login";
} }
@Autowired @Autowired
private UserRepository userRepository; // Assuming you have a repository for user operations private UserRepository userRepository; // Assuming you have a repository for user operations
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/addUsers") @GetMapping("/addUsers")
public String showAddUserForm(Model model, Authentication authentication) { public String showAddUserForm(Model model, Authentication authentication) {
List<User> allUsers = userRepository.findAll(); List<User> allUsers = userRepository.findAll();
Iterator<User> iterator = allUsers.iterator(); Iterator<User> iterator = allUsers.iterator();
while(iterator.hasNext()) { while (iterator.hasNext()) {
User user = iterator.next(); User user = iterator.next();
if(user != null) { if (user != null) {
for (Authority authority : user.getAuthorities()) { for (Authority authority : user.getAuthorities()) {
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) { if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
iterator.remove(); iterator.remove();
@ -69,7 +71,6 @@ public class AccountWebController {
return "addUsers"; return "addUsers";
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@GetMapping("/account") @GetMapping("/account")
public String account(HttpServletRequest request, Model model, Authentication authentication) { public String account(HttpServletRequest request, Model model, Authentication authentication) {
@ -87,7 +88,9 @@ public class AccountWebController {
String username = userDetails.getUsername(); String username = userDetails.getUsername();
// Fetch user details from the database // Fetch user details from the database
Optional<User> user = userRepository.findByUsername(username); // Assuming findByUsername method exists Optional<User> user =
userRepository.findByUsername(
username); // Assuming findByUsername method exists
if (!user.isPresent()) { if (!user.isPresent()) {
// Handle error appropriately // Handle error appropriately
return "redirect:/error"; // Example redirection in case of error return "redirect:/error"; // Example redirection in case of error
@ -116,10 +119,10 @@ public class AccountWebController {
return "account"; return "account";
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@GetMapping("/change-creds") @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()) { if (authentication == null || !authentication.isAuthenticated()) {
return "redirect:/"; return "redirect:/";
} }
@ -134,7 +137,9 @@ public class AccountWebController {
String username = userDetails.getUsername(); String username = userDetails.getUsername();
// Fetch user details from the database // Fetch user details from the database
Optional<User> user = userRepository.findByUsername(username); // Assuming findByUsername method exists Optional<User> user =
userRepository.findByUsername(
username); // Assuming findByUsername method exists
if (!user.isPresent()) { if (!user.isPresent()) {
// Handle error appropriately // Handle error appropriately
return "redirect:/error"; // Example redirection in case of error return "redirect:/error"; // Example redirection in case of error
@ -147,6 +152,4 @@ public class AccountWebController {
} }
return "change-creds"; return "change-creds";
} }
} }

View File

@ -25,6 +25,7 @@ public class ConverterWebController {
model.addAttribute("currentPage", "html-to-pdf"); model.addAttribute("currentPage", "html-to-pdf");
return "convert/html-to-pdf"; return "convert/html-to-pdf";
} }
@GetMapping("/markdown-to-pdf") @GetMapping("/markdown-to-pdf")
@Hidden @Hidden
public String convertMarkdownToPdfForm(Model model) { public String convertMarkdownToPdfForm(Model model) {
@ -32,7 +33,6 @@ public class ConverterWebController {
return "convert/markdown-to-pdf"; return "convert/markdown-to-pdf";
} }
@GetMapping("/url-to-pdf") @GetMapping("/url-to-pdf")
@Hidden @Hidden
public String convertURLToPdfForm(Model model) { public String convertURLToPdfForm(Model model) {
@ -40,7 +40,6 @@ public class ConverterWebController {
return "convert/url-to-pdf"; return "convert/url-to-pdf";
} }
@GetMapping("/pdf-to-img") @GetMapping("/pdf-to-img")
@Hidden @Hidden
public String pdfToimgForm(Model model) { public String pdfToimgForm(Model model) {
@ -55,9 +54,7 @@ public class ConverterWebController {
return "convert/file-to-pdf"; return "convert/file-to-pdf";
} }
// PDF TO......
//PDF TO......
@GetMapping("/pdf-to-html") @GetMapping("/pdf-to-html")
@Hidden @Hidden
@ -107,7 +104,6 @@ public class ConverterWebController {
return modelAndView; return modelAndView;
} }
@GetMapping("/pdf-to-pdfa") @GetMapping("/pdf-to-pdfa")
@Hidden @Hidden
public String pdfToPdfAForm(Model model) { public String pdfToPdfAForm(Model model) {

View File

@ -34,8 +34,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
public class GeneralWebController { public class GeneralWebController {
@GetMapping("/pipeline") @GetMapping("/pipeline")
@Hidden @Hidden
public String pipelineForm(Model model) { public String pipelineForm(Model model) {
@ -124,7 +122,6 @@ public class GeneralWebController {
return "multi-tool"; return "multi-tool";
} }
@GetMapping("/remove-pages") @GetMapping("/remove-pages")
@Hidden @Hidden
public String pageDeleter(Model model) { public String pageDeleter(Model model) {
@ -182,7 +179,6 @@ public class GeneralWebController {
return "multi-page-layout"; return "multi-page-layout";
} }
@GetMapping("/scale-pages") @GetMapping("/scale-pages")
@Hidden @Hidden
public String scalePagesFrom(Model model) { public String scalePagesFrom(Model model) {
@ -190,7 +186,6 @@ public class GeneralWebController {
return "scale-pages"; return "scale-pages";
} }
@GetMapping("/split-by-size-or-count") @GetMapping("/split-by-size-or-count")
@Hidden @Hidden
public String splitBySizeOrCount(Model model) { public String splitBySizeOrCount(Model model) {
@ -205,9 +200,7 @@ public class GeneralWebController {
return "overlay-pdf"; return "overlay-pdf";
} }
@Autowired private ResourceLoader resourceLoader;
@Autowired
private ResourceLoader resourceLoader;
private List<FontResource> getFontNames() { private List<FontResource> getFontNames() {
List<FontResource> fontNames = new ArrayList<>(); List<FontResource> fontNames = new ArrayList<>();
@ -223,10 +216,12 @@ public class GeneralWebController {
private List<FontResource> getFontNamesFromLocation(String locationPattern) { private List<FontResource> getFontNamesFromLocation(String locationPattern) {
try { try {
Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(resourceLoader) Resource[] resources =
ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
.getResources(locationPattern); .getResources(locationPattern);
return Arrays.stream(resources) return Arrays.stream(resources)
.map(resource -> { .map(
resource -> {
try { try {
String filename = resource.getFilename(); String filename = resource.getFilename();
if (filename != null) { if (filename != null) {
@ -249,23 +244,28 @@ public class GeneralWebController {
} }
} }
public String getFormatFromExtension(String extension) { public String getFormatFromExtension(String extension) {
switch (extension) { switch (extension) {
case "ttf": return "truetype"; case "ttf":
case "woff": return "woff"; return "truetype";
case "woff2": return "woff2"; case "woff":
case "eot": return "embedded-opentype"; return "woff";
case "svg": return "svg"; case "woff2":
default: return ""; // or throw an exception if an unexpected extension is encountered return "woff2";
case "eot":
return "embedded-opentype";
case "svg":
return "svg";
default:
return ""; // or throw an exception if an unexpected extension is encountered
} }
} }
public class FontResource { public class FontResource {
private String name; private String name;
private String extension; private String extension;
private String type; private String type;
public FontResource(String name, String extension) { public FontResource(String name, String extension) {
this.name = name; this.name = name;
this.extension = extension; this.extension = extension;
@ -295,11 +295,8 @@ public class GeneralWebController {
public void setType(String type) { public void setType(String type) {
this.type = type; this.type = type;
} }
} }
@GetMapping("/crop") @GetMapping("/crop")
@Hidden @Hidden
public String cropForm(Model model) { public String cropForm(Model model) {
@ -307,7 +304,6 @@ public class GeneralWebController {
return "crop"; return "crop";
} }
@GetMapping("/auto-split-pdf") @GetMapping("/auto-split-pdf")
@Hidden @Hidden
public String autoSPlitPDFForm(Model model) { public String autoSPlitPDFForm(Model model) {

View File

@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Controller @Controller
@ -20,8 +21,6 @@ public class HomeWebController {
return "about"; return "about";
} }
@GetMapping("/") @GetMapping("/")
public String home(Model model) { public String home(Model model) {
model.addAttribute("currentPage", "home"); model.addAttribute("currentPage", "home");
@ -33,20 +32,17 @@ public class HomeWebController {
return "redirect:/"; return "redirect:/";
} }
@Autowired @Autowired ApplicationProperties applicationProperties;
ApplicationProperties applicationProperties;
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE) @GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody @ResponseBody
@Hidden @Hidden
public String getRobotsTxt() { public String getRobotsTxt() {
Boolean allowGoogle = applicationProperties.getSystem().getGooglevisibility(); Boolean allowGoogle = applicationProperties.getSystem().getGooglevisibility();
if(Boolean.TRUE.equals(allowGoogle)) { if (Boolean.TRUE.equals(allowGoogle)) {
return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /"; return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /";
} else { } else {
return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /"; return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /";
} }
} }
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.web; package stirling.software.SPDF.controller.web;
import java.time.Duration; import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Comparator; import java.util.Comparator;
@ -22,6 +23,7 @@ import io.micrometer.core.instrument.MeterRegistry;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.config.StartupApplicationListener; import stirling.software.SPDF.config.StartupApplicationListener;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@ -31,10 +33,7 @@ import stirling.software.SPDF.model.ApplicationProperties;
@Tag(name = "Info", description = "Info APIs") @Tag(name = "Info", description = "Info APIs")
public class MetricsController { public class MetricsController {
@Autowired ApplicationProperties applicationProperties;
@Autowired
ApplicationProperties applicationProperties;
private final MeterRegistry meterRegistry; private final MeterRegistry meterRegistry;
@ -43,8 +42,7 @@ public class MetricsController {
@PostConstruct @PostConstruct
public void init() { public void init() {
Boolean metricsEnabled = applicationProperties.getMetrics().getEnabled(); Boolean metricsEnabled = applicationProperties.getMetrics().getEnabled();
if(metricsEnabled == null) if (metricsEnabled == null) metricsEnabled = true;
metricsEnabled = true;
this.metricsEnabled = metricsEnabled; this.metricsEnabled = metricsEnabled;
} }
@ -53,8 +51,10 @@ public class MetricsController {
} }
@GetMapping("/status") @GetMapping("/status")
@Operation(summary = "Application status and version", @Operation(
description = "This endpoint returns the status of the application and its version number.") summary = "Application status and version",
description =
"This endpoint returns the status of the application and its version number.")
public ResponseEntity<?> getStatus() { public ResponseEntity<?> getStatus() {
if (!metricsEnabled) { if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
@ -67,9 +67,13 @@ public class MetricsController {
} }
@GetMapping("/loads") @GetMapping("/loads")
@Operation(summary = "GET request count", @Operation(
description = "This endpoint returns the total count of GET requests or the count of GET requests for a specific endpoint.") summary = "GET request count",
public ResponseEntity<?> getPageLoads(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional<String> endpoint) { description =
"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) { if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
} }
@ -83,11 +87,15 @@ public class MetricsController {
if (method != null && method.equals("GET")) { if (method != null && method.equals("GET")) {
if (endpoint.isPresent() && !endpoint.get().isBlank()) { if (endpoint.isPresent() && !endpoint.get().isBlank()) {
if(!endpoint.get().startsWith("/")) { if (!endpoint.get().startsWith("/")) {
endpoint = Optional.of("/" + endpoint.get()); endpoint = Optional.of("/" + endpoint.get());
} }
System.out.println("loads " + endpoint.get() + " vs " + meter.getId().getTag("uri")); System.out.println(
if(endpoint.get().equals(meter.getId().getTag("uri"))){ "loads "
+ endpoint.get()
+ " vs "
+ meter.getId().getTag("uri"));
if (endpoint.get().equals(meter.getId().getTag("uri"))) {
if (meter instanceof Counter) { if (meter instanceof Counter) {
count += ((Counter) meter).count(); count += ((Counter) meter).count();
} }
@ -108,7 +116,8 @@ public class MetricsController {
} }
@GetMapping("/loads/all") @GetMapping("/loads/all")
@Operation(summary = "GET requests count for all endpoints", @Operation(
summary = "GET requests count for all endpoints",
description = "This endpoint returns the count of GET requests for each endpoint.") description = "This endpoint returns the count of GET requests for each endpoint.")
public ResponseEntity<?> getAllEndpointLoads() { public ResponseEntity<?> getAllEndpointLoads() {
if (!metricsEnabled) { if (!metricsEnabled) {
@ -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())) .map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
.sorted(Comparator.comparing(EndpointCount::getCount).reversed()) .sorted(Comparator.comparing(EndpointCount::getCount).reversed())
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -152,26 +162,32 @@ public class MetricsController {
this.endpoint = endpoint; this.endpoint = endpoint;
this.count = count; this.count = count;
} }
public String getEndpoint() { public String getEndpoint() {
return endpoint; return endpoint;
} }
public void setEndpoint(String endpoint) { public void setEndpoint(String endpoint) {
this.endpoint = endpoint; this.endpoint = endpoint;
} }
public double getCount() { public double getCount() {
return count; return count;
} }
public void setCount(double count) { public void setCount(double count) {
this.count = count; this.count = count;
} }
} }
@GetMapping("/requests") @GetMapping("/requests")
@Operation(summary = "POST request count", @Operation(
description = "This endpoint returns the total count of POST requests or the count of POST requests for a specific endpoint.") summary = "POST request count",
public ResponseEntity<?> getTotalRequests(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional<String> endpoint) { description =
"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) { if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
} }
@ -205,9 +221,9 @@ public class MetricsController {
} }
} }
@GetMapping("/requests/all") @GetMapping("/requests/all")
@Operation(summary = "POST requests count for all endpoints", @Operation(
summary = "POST requests count for all endpoints",
description = "This endpoint returns the count of POST requests for each endpoint.") description = "This endpoint returns the count of POST requests for each endpoint.")
public ResponseEntity<?> getAllPostRequests() { public ResponseEntity<?> getAllPostRequests() {
if (!metricsEnabled) { if (!metricsEnabled) {
@ -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())) .map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
.sorted(Comparator.comparing(EndpointCount::getCount).reversed()) .sorted(Comparator.comparing(EndpointCount::getCount).reversed())
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -243,7 +260,6 @@ public class MetricsController {
} }
} }
@GetMapping("/uptime") @GetMapping("/uptime")
public ResponseEntity<?> getUptime() { public ResponseEntity<?> getUptime() {
if (!metricsEnabled) { if (!metricsEnabled) {

View File

@ -39,7 +39,6 @@ public class OtherWebController {
return "misc/show-javascript"; return "misc/show-javascript";
} }
@GetMapping("/add-page-numbers") @GetMapping("/add-page-numbers")
@Hidden @Hidden
public String addPageNumbersForm(Model model) { public String addPageNumbersForm(Model model) {
@ -61,8 +60,6 @@ public class OtherWebController {
return "misc/flatten"; return "misc/flatten";
} }
@GetMapping("/change-metadata") @GetMapping("/change-metadata")
@Hidden @Hidden
public String addWatermarkForm(Model model) { public String addWatermarkForm(Model model) {
@ -83,8 +80,11 @@ public class OtherWebController {
if (files == null) { if (files == null) {
return Collections.emptyList(); return Collections.emptyList();
} }
return Arrays.stream(files).filter(file -> file.getName().endsWith(".traineddata")).map(file -> file.getName().replace(".traineddata", "")) return Arrays.stream(files)
.filter(lang -> !lang.equalsIgnoreCase("osd")).collect(Collectors.toList()); .filter(file -> file.getName().endsWith(".traineddata"))
.map(file -> file.getName().replace(".traineddata", ""))
.filter(lang -> !lang.equalsIgnoreCase("osd"))
.collect(Collectors.toList());
} }
@GetMapping("/ocr-pdf") @GetMapping("/ocr-pdf")
@ -98,7 +98,6 @@ public class OtherWebController {
return modelAndView; return modelAndView;
} }
@GetMapping("/add-image") @GetMapping("/add-image")
@Hidden @Hidden
public String overlayImage(Model model) { public String overlayImage(Model model) {
@ -147,7 +146,4 @@ public class OtherWebController {
model.addAttribute("currentPage", "auto-rename"); model.addAttribute("currentPage", "auto-rename");
return "misc/auto-rename"; return "misc/auto-rename";
} }
} }

View File

@ -24,6 +24,7 @@ public class SecurityWebController {
model.addAttribute("currentPage", "add-password"); model.addAttribute("currentPage", "add-password");
return "security/add-password"; return "security/add-password";
} }
@GetMapping("/change-permissions") @GetMapping("/change-permissions")
@Hidden @Hidden
public String permissionsForm(Model model) { public String permissionsForm(Model model) {

View File

@ -13,7 +13,9 @@ public class ApiEndpoint {
public ApiEndpoint(String name, JsonNode postNode) { public ApiEndpoint(String name, JsonNode postNode) {
this.name = name; this.name = name;
this.parameters = new HashMap<>(); this.parameters = new HashMap<>();
postNode.path("parameters").forEach(paramNode -> { postNode.path("parameters")
.forEach(
paramNode -> {
String paramName = paramNode.path("name").asText(); String paramName = paramNode.path("name").asText();
parameters.put(paramName, paramNode); parameters.put(paramName, paramNode);
}); });
@ -37,6 +39,4 @@ public class ApiEndpoint {
public String toString() { public String toString() {
return "ApiEndpoint [name=" + name + ", parameters=" + parameters + "]"; return "ApiEndpoint [name=" + name + ", parameters=" + parameters + "]";
} }
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.model; package stirling.software.SPDF.model;
import java.util.Collection; import java.util.Collection;
import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AbstractAuthenticationToken;
@ -16,7 +17,8 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
setAuthenticated(false); setAuthenticated(false);
} }
public ApiKeyAuthenticationToken(Object principal, String apiKey, Collection<? extends GrantedAuthority> authorities) { public ApiKeyAuthenticationToken(
Object principal, String apiKey, Collection<? extends GrantedAuthority> authorities) {
super(authorities); super(authorities);
this.principal = principal; // principal can be a UserDetails object this.principal = principal; // principal can be a UserDetails object
this.credentials = apiKey; this.credentials = apiKey;
@ -36,7 +38,8 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
@Override @Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) { 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); super.setAuthenticated(false);
} }

View File

@ -69,7 +69,9 @@ public class ApplicationProperties {
} }
public AutomaticallyGenerated getAutomaticallyGenerated() { public AutomaticallyGenerated getAutomaticallyGenerated() {
return automaticallyGenerated != null ? automaticallyGenerated : new AutomaticallyGenerated(); return automaticallyGenerated != null
? automaticallyGenerated
: new AutomaticallyGenerated();
} }
public void setAutomaticallyGenerated(AutomaticallyGenerated automaticallyGenerated) { public void setAutomaticallyGenerated(AutomaticallyGenerated automaticallyGenerated) {
@ -78,9 +80,21 @@ public class ApplicationProperties {
@Override @Override
public String toString() { public String toString() {
return "ApplicationProperties [security=" + security + ", system=" + system + ", ui=" + ui + ", endpoints=" return "ApplicationProperties [security="
+ endpoints + ", metrics=" + metrics + ", automaticallyGenerated=" + automaticallyGenerated + security
+ ", autoPipeline=" + autoPipeline + "]"; + ", system="
+ system
+ ", ui="
+ ui
+ ", endpoints="
+ endpoints
+ ", metrics="
+ metrics
+ ", automaticallyGenerated="
+ automaticallyGenerated
+ ", autoPipeline="
+ autoPipeline
+ "]";
} }
public static class AutoPipeline { public static class AutoPipeline {
@ -98,10 +112,8 @@ public class ApplicationProperties {
public String toString() { public String toString() {
return "AutoPipeline [outputFolder=" + outputFolder + "]"; return "AutoPipeline [outputFolder=" + outputFolder + "]";
} }
} }
public static class Security { public static class Security {
private Boolean enableLogin; private Boolean enableLogin;
private Boolean csrfDisabled; private Boolean csrfDisabled;
@ -109,7 +121,6 @@ public class ApplicationProperties {
private int loginAttemptCount; private int loginAttemptCount;
private long loginResetTimeMinutes; private long loginResetTimeMinutes;
public int getLoginAttemptCount() { public int getLoginAttemptCount() {
return loginAttemptCount; return loginAttemptCount;
} }
@ -150,11 +161,15 @@ public class ApplicationProperties {
this.csrfDisabled = csrfDisabled; this.csrfDisabled = csrfDisabled;
} }
@Override @Override
public String toString() { public String toString() {
return "Security [enableLogin=" + enableLogin + ", initialLogin=" + initialLogin + ", csrfDisabled=" return "Security [enableLogin="
+ csrfDisabled + "]"; + enableLogin
+ ", initialLogin="
+ initialLogin
+ ", csrfDisabled="
+ csrfDisabled
+ "]";
} }
public static class InitialLogin { public static class InitialLogin {
@ -180,11 +195,12 @@ public class ApplicationProperties {
@Override @Override
public String toString() { 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; private Boolean enableAlphaFunctionality;
public Boolean getEnableAlphaFunctionality() { public Boolean getEnableAlphaFunctionality() {
return enableAlphaFunctionality; return enableAlphaFunctionality;
} }
@ -250,13 +263,20 @@ public class ApplicationProperties {
@Override @Override
public String toString() { public String toString() {
return "System [defaultLocale=" + defaultLocale + ", googlevisibility=" + googlevisibility return "System [defaultLocale="
+ ", rootURIPath=" + rootURIPath + ", customStaticFilePath=" + customStaticFilePath + defaultLocale
+ ", maxFileSize=" + maxFileSize + ", enableAlphaFunctionality=" + enableAlphaFunctionality + "]"; + ", googlevisibility="
+ googlevisibility
+ ", rootURIPath="
+ rootURIPath
+ ", customStaticFilePath="
+ customStaticFilePath
+ ", maxFileSize="
+ maxFileSize
+ ", enableAlphaFunctionality="
+ enableAlphaFunctionality
+ "]";
} }
} }
public static class Ui { public static class Ui {
@ -265,8 +285,7 @@ public class ApplicationProperties {
private String appNameNavbar; private String appNameNavbar;
public String getAppName() { public String getAppName() {
if(appName != null && appName.trim().length() == 0) if (appName != null && appName.trim().length() == 0) return null;
return null;
return appName; return appName;
} }
@ -275,8 +294,7 @@ public class ApplicationProperties {
} }
public String getHomeDescription() { public String getHomeDescription() {
if(homeDescription != null && homeDescription.trim().length() == 0) if (homeDescription != null && homeDescription.trim().length() == 0) return null;
return null;
return homeDescription; return homeDescription;
} }
@ -285,8 +303,7 @@ public class ApplicationProperties {
} }
public String getAppNameNavbar() { public String getAppNameNavbar() {
if(appNameNavbar != null && appNameNavbar.trim().length() == 0) if (appNameNavbar != null && appNameNavbar.trim().length() == 0) return null;
return null;
return appNameNavbar; return appNameNavbar;
} }
@ -296,11 +313,16 @@ public class ApplicationProperties {
@Override @Override
public String toString() { public String toString() {
return "UserInterface [appName=" + appName + ", homeDescription=" + homeDescription + ", appNameNavbar=" + appNameNavbar + "]"; return "UserInterface [appName="
+ appName
+ ", homeDescription="
+ homeDescription
+ ", appNameNavbar="
+ appNameNavbar
+ "]";
} }
} }
public static class Endpoints { public static class Endpoints {
private List<String> toRemove; private List<String> toRemove;
private List<String> groupsToRemove; private List<String> groupsToRemove;
@ -325,8 +347,6 @@ public class ApplicationProperties {
public String toString() { public String toString() {
return "Endpoints [toRemove=" + toRemove + ", groupsToRemove=" + groupsToRemove + "]"; return "Endpoints [toRemove=" + toRemove + ", groupsToRemove=" + groupsToRemove + "]";
} }
} }
public static class Metrics { public static class Metrics {
@ -344,8 +364,6 @@ public class ApplicationProperties {
public String toString() { public String toString() {
return "Metrics [enabled=" + enabled + "]"; return "Metrics [enabled=" + enabled + "]";
} }
} }
public static class AutomaticallyGenerated { public static class AutomaticallyGenerated {
@ -361,8 +379,9 @@ public class ApplicationProperties {
@Override @Override
public String toString() { public String toString() {
return "AutomaticallyGenerated [key=" + (key != null && !key.isEmpty() ? "MASKED" : "NULL") + "]"; return "AutomaticallyGenerated [key="
+ (key != null && !key.isEmpty() ? "MASKED" : "NULL")
+ "]";
} }
} }
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.model; package stirling.software.SPDF.model;
public class AttemptCounter { public class AttemptCounter {
private int attemptCount; private int attemptCount;
private long lastAttemptTime; private long lastAttemptTime;

View File

@ -13,10 +13,7 @@ import jakarta.persistence.Table;
@Table(name = "authorities") @Table(name = "authorities")
public class Authority { public class Authority {
public Authority() { public Authority() {}
}
public Authority(String authority, User user) { public Authority(String authority, User user) {
this.authority = authority; this.authority = authority;
@ -24,7 +21,6 @@ public class Authority {
user.getAuthorities().add(this); user.getAuthorities().add(this);
} }
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@ -59,6 +55,4 @@ public class Authority {
public void setUser(User user) { public void setUser(User user) {
this.user = user; this.user = user;
} }
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.model; package stirling.software.SPDF.model;
public class PDFText { public class PDFText {
private final int pageIndex; private final int pageIndex;
private final float x1; private final float x1;

View File

@ -56,6 +56,5 @@ public class PersistentLogin {
this.lastUsed = lastUsed; this.lastUsed = lastUsed;
} }
// Getters, setters, etc. // Getters, setters, etc.
} }

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.model; package stirling.software.SPDF.model;
import java.util.List; import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
@ -14,7 +15,6 @@ public class PipelineConfig {
@JsonProperty("outputFileName") @JsonProperty("outputFileName")
private String outputPattern; private String outputPattern;
public String getName() { public String getName() {
return name; return name;
} }
@ -46,6 +46,4 @@ public class PipelineConfig {
public void setOutputPattern(String outputPattern) { public void setOutputPattern(String outputPattern) {
this.outputPattern = outputPattern; this.outputPattern = outputPattern;
} }
} }

View File

@ -6,7 +6,6 @@ public class PipelineOperation {
private String operation; private String operation;
private Map<String, Object> parameters; private Map<String, Object> parameters;
public String getOperation() { public String getOperation() {
return operation; return operation;
} }
@ -27,6 +26,4 @@ public class PipelineOperation {
public String toString() { public String toString() {
return "PipelineOperation [operation=" + operation + ", parameters=" + parameters + "]"; return "PipelineOperation [operation=" + operation + ", parameters=" + parameters + "]";
} }
}
}

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.model; package stirling.software.SPDF.model;
public enum Role { public enum Role {
// Unlimited access // Unlimited access
@ -16,7 +17,6 @@ public enum Role {
// 0 API calls per day and 20 web calls // 0 API calls per day and 20 web calls
WEB_ONLY_USER("ROLE_WEB_ONLY_USER", 0, 20), WEB_ONLY_USER("ROLE_WEB_ONLY_USER", 0, 20),
INTERNAL_API_USER("STIRLING-PDF-BACKEND-API-USER", Integer.MAX_VALUE, Integer.MAX_VALUE), INTERNAL_API_USER("STIRLING-PDF-BACKEND-API-USER", Integer.MAX_VALUE, Integer.MAX_VALUE),
DEMO_USER("ROLE_DEMO_USER", 100, 100); DEMO_USER("ROLE_DEMO_USER", 100, 100);
@ -51,5 +51,4 @@ public enum Role {
} }
throw new IllegalArgumentException("No Role defined for id: " + roleId); throw new IllegalArgumentException("No Role defined for id: " + roleId);
} }
} }

View File

@ -1,4 +1,12 @@
package stirling.software.SPDF.model; package stirling.software.SPDF.model;
public enum SortTypes { public enum SortTypes {
REVERSE_ORDER, DUPLEX_SORT, BOOKLET_SORT, SIDE_STITCH_BOOKLET_SORT, ODD_EVEN_SPLIT, REMOVE_FIRST, REMOVE_LAST, REMOVE_FIRST_AND_LAST, REVERSE_ORDER,
DUPLEX_SORT,
BOOKLET_SORT,
SIDE_STITCH_BOOKLET_SORT,
ODD_EVEN_SPLIT,
REMOVE_FIRST,
REMOVE_LAST,
REMOVE_FIRST_AND_LAST,
} }

View File

@ -19,6 +19,7 @@ import jakarta.persistence.JoinColumn;
import jakarta.persistence.MapKeyColumn; import jakarta.persistence.MapKeyColumn;
import jakarta.persistence.OneToMany; import jakarta.persistence.OneToMany;
import jakarta.persistence.Table; import jakarta.persistence.Table;
@Entity @Entity
@Table(name = "users") @Table(name = "users")
public class User { public class User {
@ -52,7 +53,6 @@ public class User {
@CollectionTable(name = "user_settings", joinColumns = @JoinColumn(name = "user_id")) @CollectionTable(name = "user_settings", joinColumns = @JoinColumn(name = "user_id"))
private Map<String, String> settings = new HashMap<>(); // Key-value pairs of settings. private Map<String, String> settings = new HashMap<>(); // Key-value pairs of settings.
public boolean isFirstLogin() { public boolean isFirstLogin() {
return isFirstLogin != null && isFirstLogin; return isFirstLogin != null && isFirstLogin;
} }
@ -120,6 +120,7 @@ public class User {
public void addAuthorities(Set<Authority> authorities) { public void addAuthorities(Set<Authority> authorities) {
this.authorities.addAll(authorities); this.authorities.addAll(authorities);
} }
public void addAuthority(Authority authorities) { public void addAuthority(Authority authorities) {
this.authorities.add(authorities); this.authorities.add(authorities);
} }
@ -129,6 +130,4 @@ public class User {
.map(Authority::getAuthority) .map(Authority::getAuthority)
.collect(Collectors.joining(", ")); .collect(Collectors.joining(", "));
} }
} }

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