diff --git a/.gitignore b/.gitignore
index 0a5cd198..661ca292 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,115 +1,122 @@
-
-
-### Eclipse ###
-.metadata
-bin/
-tmp/
-*.tmp
-*.bak
-*.swp
-*~.nib
-local.properties
-.settings/
-.loadpath
-.recommenders
-.classpath
-.project
-version.properties
-
-# Gradle
-.gradle
-.lock
-
-# External tool builders
-.externalToolBuilders/
-
-# Locally stored "Eclipse launch configurations"
-*.launch
-
-# PyDev specific (Python IDE for Eclipse)
-*.pydevproject
-
-# CDT-specific (C/C++ Development Tooling)
-.cproject
-
-# CDT- autotools
-.autotools
-
-# Java annotation processor (APT)
-.factorypath
-
-# PDT-specific (PHP Development Tools)
-.buildpath
-
-# sbteclipse plugin
-.target
-
-# Tern plugin
-.tern-project
-
-# TeXlipse plugin
-.texlipse
-
-# STS (Spring Tool Suite)
-.springBeans
-
-# Code Recommenders
-.recommenders/
-
-# Annotation Processing
-.apt_generated/
-.apt_generated_test/
-
-# Scala IDE specific (Scala & Java development for Eclipse)
-.cache-main
-.scala_dependencies
-.worksheet
-
-# Uncomment this line if you wish to ignore the project description file.
-# Typically, this file would be tracked if it contains build/dependency configurations:
-#.project
-
-### Eclipse Patch ###
-# Spring Boot Tooling
-.sts4-cache/
-
-### Git ###
-# Created by git for backups. To disable backups in Git:
-# $ git config --global mergetool.keepBackup false
-*.orig
-
-# Created by git when using merge tools for conflicts
-*.BACKUP.*
-*.BASE.*
-*.LOCAL.*
-*.REMOTE.*
-*_BACKUP_*.txt
-*_BASE_*.txt
-*_LOCAL_*.txt
-*_REMOTE_*.txt
-
-### Java ###
-# Compiled class file
-*.class
-
-# Log file
-*.log
-
-# BlueJ files
-*.ctxt
-
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
-
-# Package Files #
-*.jar
-*.war
-*.nar
-*.ear
-*.zip
-*.tar.gz
-*.rar
-
-/build
-
+
+
+### Eclipse ###
+.metadata
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.settings/
+.loadpath
+.recommenders
+.classpath
+.project
+version.properties
+pipeline/
+
+#### Stirling-PDF Files ###
+customFiles/
+config/
+watchedFolders/
+
+
+# Gradle
+.gradle
+.lock
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
+
+# PyDev specific (Python IDE for Eclipse)
+*.pydevproject
+
+# CDT-specific (C/C++ Development Tooling)
+.cproject
+
+# CDT- autotools
+.autotools
+
+# Java annotation processor (APT)
+.factorypath
+
+# PDT-specific (PHP Development Tools)
+.buildpath
+
+# sbteclipse plugin
+.target
+
+# Tern plugin
+.tern-project
+
+# TeXlipse plugin
+.texlipse
+
+# STS (Spring Tool Suite)
+.springBeans
+
+# Code Recommenders
+.recommenders/
+
+# Annotation Processing
+.apt_generated/
+.apt_generated_test/
+
+# Scala IDE specific (Scala & Java development for Eclipse)
+.cache-main
+.scala_dependencies
+.worksheet
+
+# Uncomment this line if you wish to ignore the project description file.
+# Typically, this file would be tracked if it contains build/dependency configurations:
+#.project
+
+### Eclipse Patch ###
+# Spring Boot Tooling
+.sts4-cache/
+
+### Git ###
+# Created by git for backups. To disable backups in Git:
+# $ git config --global mergetool.keepBackup false
+*.orig
+
+# Created by git when using merge tools for conflicts
+*.BACKUP.*
+*.BASE.*
+*.LOCAL.*
+*.REMOTE.*
+*_BACKUP_*.txt
+*_BASE_*.txt
+*_LOCAL_*.txt
+*_REMOTE_*.txt
+
+### Java ###
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+/build
+
/.vscode
\ No newline at end of file
diff --git a/Dockerfile-lite b/Dockerfile-lite
index d3968a2a..eb92e487 100644
--- a/Dockerfile-lite
+++ b/Dockerfile-lite
@@ -10,6 +10,12 @@ RUN apt-get update && \
unoconv && \
rm -rf /var/lib/apt/lists/*
+#Install fonts
+RUN mkdir /usr/share/fonts/opentype/noto/
+COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
+COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
+RUN fc-cache -f -v
+
# Copy the application JAR file
COPY build/libs/*.jar app.jar
diff --git a/Endpoint-groups.md b/Endpoint-groups.md
index 6692a9a2..e6401396 100644
--- a/Endpoint-groups.md
+++ b/Endpoint-groups.md
@@ -1,35 +1,41 @@
-| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript |
-|---------------------|---------|---------|----------|-------|------|--------|--------|-------------|----------|----------|------------|
-| merge-pdfs | ✔️ | | | | | | | | | ✔️ | |
-| multi-page-layout | ✔️ | | | | | | | | | ✔️ | |
-| pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ |
-| remove-pages | ✔️ | | | | | | | | | ✔️ | |
-| rotate-pdf | ✔️ | | | | | | | | | ✔️ | |
-| scale-pages | ✔️ | | | | | | | | | ✔️ | |
-| split-pdfs | ✔️ | | | | | | | | | ✔️ | |
-| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
-| img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
-| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
-| pdf-to-img | | ✔️ | | | | | | | | ✔️ | |
-| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
-| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
-| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | |
-| pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | |
-| pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | |
-| xlsx-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
-| add-password | | | ✔️ | | | | | | | ✔️ | |
-| add-watermark | | | ✔️ | | | | | | | ✔️ | |
-| cert-sign | | | ✔️ | | | | | | | ✔️ | |
-| change-permissions | | | ✔️ | | | | | | | ✔️ | |
-| remove-password | | | ✔️ | | | | | | | ✔️ | |
-| add-image | | | | ✔️ | | | | | | ✔️ | |
-| change-metadata | | | | ✔️ | | | | | | ✔️ | |
-| compare | | | | ✔️ | | | | | | | ✔️ |
-| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
-| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
-| extract-images | | | | ✔️ | | | | | | ✔️ | |
-| flatten | | | | ✔️ | | | | | | | |
-| ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
-| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
-| repair | | | | ✔️ | ✔️ | | | ✔️ | | | |
-| sign | | | | ✔️ | | | | | | | ✔️ |
+| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript |
+|---------------------|---------|---------|----------|-------|------|--------|--------|-------------|----------|----------|------------|
+| adjust-contrast | ✔️ | | | | | | | | | | ✔️ |
+| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | |
+| crop | ✔️ | | | | | | | | | ✔️ | |
+| merge-pdfs | ✔️ | | | | | | | | | ✔️ | |
+| multi-page-layout | ✔️ | | | | | | | | | ✔️ | |
+| pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ |
+| remove-pages | ✔️ | | | | | | | | | ✔️ | |
+| rotate-pdf | ✔️ | | | | | | | | | ✔️ | |
+| scale-pages | ✔️ | | | | | | | | | ✔️ | |
+| split-pdfs | ✔️ | | | | | | | | | ✔️ | |
+| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
+| img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
+| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
+| pdf-to-img | | ✔️ | | | | | | | | ✔️ | |
+| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
+| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
+| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | |
+| pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | |
+| pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | |
+| xlsx-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
+| add-password | | | ✔️ | | | | | | | ✔️ | |
+| add-watermark | | | ✔️ | | | | | | | ✔️ | |
+| cert-sign | | | ✔️ | | | | | | | ✔️ | |
+| change-permissions | | | ✔️ | | | | | | | ✔️ | |
+| remove-password | | | ✔️ | | | | | | | ✔️ | |
+| sanitize-pdf | | | ✔️ | | | | | | | ✔️ | |
+| add-image | | | | ✔️ | | | | | | ✔️ | |
+| add-page-numbers | | | | ✔️ | | | | | | ✔️ | |
+| auto-rename | | | | ✔️ | | | | | | ✔️ | |
+| change-metadata | | | | ✔️ | | | | | | ✔️ | |
+| compare | | | | ✔️ | | | | | | | ✔️ |
+| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
+| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
+| extract-images | | | | ✔️ | | | | | | ✔️ | |
+| flatten | | | | ✔️ | | | | | | | |
+| ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
+| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
+| repair | | | | ✔️ | ✔️ | | | ✔️ | | | |
+| sign | | | | ✔️ | | | | | | | ✔️ |
\ No newline at end of file
diff --git a/README.md b/README.md
index 0398bf23..b2606c7d 100644
--- a/README.md
+++ b/README.md
@@ -86,6 +86,8 @@ docker run -d \
Can also add these for customisation but are not required
+ -v /location/of/extraConfigs:/configs \
+ -v /location/of/customFiles:/customFiles \
-e APP_HOME_NAME="Stirling PDF" \
-e APP_HOME_DESCRIPTION="Your locally hosted one-stop-shop for all your PDF needs." \
-e APP_NAVBAR_NAME="Stirling PDF" \
@@ -104,6 +106,7 @@ services:
volumes:
- /location/of/trainingData:/usr/share/tesseract-ocr/4.00/tessdata #Required for extra OCR languages
# - /location/of/extraConfigs:/configs
+# - /location/of/customFiles:/customFiles/
# environment:
# APP_LOCALE: en_GB
# APP_HOME_NAME: Stirling PDF
@@ -161,10 +164,11 @@ Using the same method you can also change
- Change root URI for Stirling-PDF ie change server.com/ to server.com/pdf-app by running APP_ROOT_PATH as pdf-app
- Disable and remove endpoints and functionality from Stirling-PDF. Currently the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma seperated lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image to pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Frooodle/Stirling-PDF/blob/main/groups.md)
- Change the max file size allowed through the server with the environment variable MAX_FILE_SIZE. default 2000MB
+- Customise static files such as app logo by placing files in the /customFiles/static/ directory. Example to customise app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF
## API
For those wanting to use Stirling-PDFs backend API to link with their own custom scripting to edit PDFs you can view all existing API documentation
-[here](https://app.swaggerhub.com/apis-docs/Frooodle/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation
+[here](https://app.swaggerhub.com/apis-docs/Frooodle/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF)
## FAQ
diff --git a/Version-groups.md b/Version-groups.md
index 244284c7..557cadb2 100644
--- a/Version-groups.md
+++ b/Version-groups.md
@@ -1,48 +1,54 @@
-|Technology | Ultra-Lite | Lite | Full |
-|----------------|:----------:|:----:|:----:|
-| Java | ✔️ | ✔️ | ✔️ |
-| JavaScript | ✔️ | ✔️ | ✔️ |
-| Libre | | ✔️ | ✔️ |
-| Python | | | ✔️ |
-| OpenCV | | | ✔️ |
-| OCRmyPDF | | | ✔️ |
-
-
-
-
-
-Operation | Ultra-Lite | Lite | Full
---------------------|------------|------|-----
-add-password | ✔️ | ✔️ | ✔️
-add-watermark | ✔️ | ✔️ | ✔️
-cert-sign | ✔️ | ✔️ | ✔️
-change-metadata | ✔️ | ✔️ | ✔️
-change-permissions | ✔️ | ✔️ | ✔️
-compare | ✔️ | ✔️ | ✔️
-extract-images | ✔️ | ✔️ | ✔️
-flatten | ✔️ | ✔️ | ✔️
-img-to-pdf | ✔️ | ✔️ | ✔️
-merge-pdfs | ✔️ | ✔️ | ✔️
-multi-page-layout | ✔️ | ✔️ | ✔️
-pdf-organizer | ✔️ | ✔️ | ✔️
-pdf-to-img | ✔️ | ✔️ | ✔️
-remove-pages | ✔️ | ✔️ | ✔️
-remove-password | ✔️ | ✔️ | ✔️
-rotate-pdf | ✔️ | ✔️ | ✔️
-scale-pages | ✔️ | ✔️ | ✔️
-sign | ✔️ | ✔️ | ✔️
-split-pdfs | ✔️ | ✔️ | ✔️
-add-image | ✔️ | ✔️ | ✔️
-file-to-pdf | | ✔️ | ✔️
-pdf-to-html | | ✔️ | ✔️
-pdf-to-presentation | | ✔️ | ✔️
-pdf-to-text | | ✔️ | ✔️
-pdf-to-word | | ✔️ | ✔️
-pdf-to-xml | | ✔️ | ✔️
-repair | | ✔️ | ✔️
-xlsx-to-pdf | | ✔️ | ✔️
-compress-pdf | | | ✔️
-extract-image-scans | | | ✔️
-ocr-pdf | | | ✔️
-pdf-to-pdfa | | | ✔️
-remove-blanks | | | ✔️
+|Technology | Ultra-Lite | Lite | Full |
+|----------------|:----------:|:----:|:----:|
+| Java | ✔️ | ✔️ | ✔️ |
+| JavaScript | ✔️ | ✔️ | ✔️ |
+| Libre | | ✔️ | ✔️ |
+| Python | | | ✔️ |
+| OpenCV | | | ✔️ |
+| OCRmyPDF | | | ✔️ |
+
+
+
+
+
+Operation | Ultra-Lite | Lite | Full
+--------------------|------------|------|-----
+add-page-numbers | ✔️ | ✔️ | ✔️
+add-password | ✔️ | ✔️ | ✔️
+add-watermark | ✔️ | ✔️ | ✔️
+adjust-contrast | ✔️ | ✔️ | ✔️
+auto-split-pdf | ✔️ | ✔️ | ✔️
+auto-rename | ✔️ | ✔️ | ✔️
+cert-sign | ✔️ | ✔️ | ✔️
+crop | ✔️ | ✔️ | ✔️
+change-metadata | ✔️ | ✔️ | ✔️
+change-permissions | ✔️ | ✔️ | ✔️
+compare | ✔️ | ✔️ | ✔️
+extract-images | ✔️ | ✔️ | ✔️
+flatten | ✔️ | ✔️ | ✔️
+img-to-pdf | ✔️ | ✔️ | ✔️
+merge-pdfs | ✔️ | ✔️ | ✔️
+multi-page-layout | ✔️ | ✔️ | ✔️
+pdf-organizer | ✔️ | ✔️ | ✔️
+pdf-to-img | ✔️ | ✔️ | ✔️
+remove-pages | ✔️ | ✔️ | ✔️
+remove-password | ✔️ | ✔️ | ✔️
+rotate-pdf | ✔️ | ✔️ | ✔️
+sanitize-pdf | ✔️ | ✔️ | ✔️
+scale-pages | ✔️ | ✔️ | ✔️
+sign | ✔️ | ✔️ | ✔️
+split-pdfs | ✔️ | ✔️ | ✔️
+add-image | ✔️ | ✔️ | ✔️
+file-to-pdf | | ✔️ | ✔️
+pdf-to-html | | ✔️ | ✔️
+pdf-to-presentation | | ✔️ | ✔️
+pdf-to-text | | ✔️ | ✔️
+pdf-to-word | | ✔️ | ✔️
+pdf-to-xml | | ✔️ | ✔️
+repair | | ✔️ | ✔️
+xlsx-to-pdf | | ✔️ | ✔️
+compress-pdf | | | ✔️
+extract-image-scans | | | ✔️
+ocr-pdf | | | ✔️
+pdf-to-pdfa | | | ✔️
+remove-blanks | | | ✔️
diff --git a/build.gradle b/build.gradle
index 9d69535c..0afc366b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -8,7 +8,7 @@ plugins {
}
group = 'stirling.software'
-version = '0.10.3'
+version = '0.11.0'
sourceCompatibility = '17'
repositories {
@@ -62,6 +62,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-core'
+ implementation group: 'com.google.zxing', name: 'core', version: '3.5.1'
+
developmentOnly("org.springframework.boot:spring-boot-devtools")
}
diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java
index 02f66c22..f9512295 100644
--- a/src/main/java/stirling/software/SPDF/SPdfApplication.java
+++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java
@@ -1,63 +1,76 @@
-package stirling.software.SPDF;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.core.env.Environment;
-import org.springframework.scheduling.annotation.EnableScheduling;
-
-import jakarta.annotation.PostConstruct;
-
-@SpringBootApplication
-//@EnableScheduling
-public class SPdfApplication {
-
- @Autowired
- private Environment env;
-
- @PostConstruct
- public void init() {
- // Check if the BROWSER_OPEN environment variable is set to true
- String browserOpenEnv = env.getProperty("BROWSER_OPEN");
- boolean browserOpen = browserOpenEnv != null && browserOpenEnv.equalsIgnoreCase("true");
-
- if (browserOpen) {
- try {
- String port = env.getProperty("local.server.port");
- if(port == null || port.length() == 0) {
- port="8080";
- }
- String url = "http://localhost:" + port;
-
- String os = System.getProperty("os.name").toLowerCase();
- Runtime rt = Runtime.getRuntime();
- if (os.contains("win")) {
- // For Windows
- rt.exec("rundll32 url.dll,FileProtocolHandler " + url);
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
-
- public static void main(String[] args) {
- SpringApplication.run(SPdfApplication.class, args);
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- System.out.println("Stirling-PDF Started.");
-
- String port = System.getProperty("local.server.port");
- if(port == null || port.length() == 0) {
- port="8080";
- }
- String url = "http://localhost:" + port;
- System.out.println("Navigate to " + url);
- }
-
-
+package stirling.software.SPDF;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.core.env.Environment;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+import jakarta.annotation.PostConstruct;
+import stirling.software.SPDF.utils.GeneralUtils;
+
+@SpringBootApplication
+//@EnableScheduling
+public class SPdfApplication {
+
+ @Autowired
+ private Environment env;
+
+ @PostConstruct
+ public void init() {
+ // Check if the BROWSER_OPEN environment variable is set to true
+ String browserOpenEnv = env.getProperty("BROWSER_OPEN");
+ boolean browserOpen = browserOpenEnv != null && browserOpenEnv.equalsIgnoreCase("true");
+
+ if (browserOpen) {
+ try {
+ String port = env.getProperty("local.server.port");
+ if(port == null || port.length() == 0) {
+ port="8080";
+ }
+ String url = "http://localhost:" + port;
+
+ String os = System.getProperty("os.name").toLowerCase();
+ Runtime rt = Runtime.getRuntime();
+ if (os.contains("win")) {
+ // For Windows
+ rt.exec("rundll32 url.dll,FileProtocolHandler " + url);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public static void main(String[] args) {
+ SpringApplication.run(SPdfApplication.class, args);
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ GeneralUtils.createDir("customFiles/static/");
+ GeneralUtils.createDir("customFiles/templates/");
+ GeneralUtils.createDir("config");
+
+
+
+ System.out.println("Stirling-PDF Started.");
+
+ String port = System.getProperty("local.server.port");
+ if(port == null || port.length() == 0) {
+ port="8080";
+ }
+ String url = "http://localhost:" + port;
+ System.out.println("Navigate to " + url);
+ }
+
+
}
\ No newline at end of file
diff --git a/src/main/java/stirling/software/SPDF/config/Beans.java b/src/main/java/stirling/software/SPDF/config/Beans.java
index 7a963ba4..f8ff302e 100644
--- a/src/main/java/stirling/software/SPDF/config/Beans.java
+++ b/src/main/java/stirling/software/SPDF/config/Beans.java
@@ -1,60 +1,60 @@
-package stirling.software.SPDF.config;
-
-import java.util.Locale;
-
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.web.servlet.LocaleResolver;
-import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
-import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
-import org.springframework.web.servlet.i18n.SessionLocaleResolver;
-
-@Configuration
-public class Beans implements WebMvcConfigurer {
-
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- registry.addInterceptor(localeChangeInterceptor());
- registry.addInterceptor(new CleanUrlInterceptor());
- }
-
- @Bean
- public LocaleChangeInterceptor localeChangeInterceptor() {
- LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
- lci.setParamName("lang");
- return lci;
- }
-
- @Bean
- public LocaleResolver localeResolver() {
- SessionLocaleResolver slr = new SessionLocaleResolver();
-
- String appLocaleEnv = System.getProperty("APP_LOCALE");
- if (appLocaleEnv == null)
- appLocaleEnv = System.getenv("APP_LOCALE");
- Locale defaultLocale = Locale.UK; // Fallback to UK locale if environment variable is not set
-
- if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
- Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
- String tempLanguageTag = tempLocale.toLanguageTag();
-
- if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
- defaultLocale = tempLocale;
- } else {
- tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_","-"));
- tempLanguageTag = tempLocale.toLanguageTag();
-
- if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
- defaultLocale = tempLocale;
- } else {
- System.err.println("Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK.");
- }
- }
- }
-
- slr.setDefaultLocale(defaultLocale);
- return slr;
- }
-
-}
+package stirling.software.SPDF.config;
+
+import java.util.Locale;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.LocaleResolver;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
+import org.springframework.web.servlet.i18n.SessionLocaleResolver;
+
+@Configuration
+public class Beans implements WebMvcConfigurer {
+
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+ registry.addInterceptor(localeChangeInterceptor());
+ registry.addInterceptor(new CleanUrlInterceptor());
+ }
+
+ @Bean
+ public LocaleChangeInterceptor localeChangeInterceptor() {
+ LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
+ lci.setParamName("lang");
+ return lci;
+ }
+
+ @Bean
+ public LocaleResolver localeResolver() {
+ SessionLocaleResolver slr = new SessionLocaleResolver();
+
+ String appLocaleEnv = System.getProperty("APP_LOCALE");
+ if (appLocaleEnv == null)
+ appLocaleEnv = System.getenv("APP_LOCALE");
+ Locale defaultLocale = Locale.UK; // Fallback to UK locale if environment variable is not set
+
+ if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
+ Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
+ String tempLanguageTag = tempLocale.toLanguageTag();
+
+ if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
+ defaultLocale = tempLocale;
+ } else {
+ tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_","-"));
+ tempLanguageTag = tempLocale.toLanguageTag();
+
+ if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
+ defaultLocale = tempLocale;
+ } else {
+ System.err.println("Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK.");
+ }
+ }
+ }
+
+ slr.setDefaultLocale(defaultLocale);
+ return slr;
+ }
+
+}
diff --git a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java
index 92580b43..24f2822d 100644
--- a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java
+++ b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java
@@ -1,200 +1,208 @@
-package stirling.software.SPDF.config;
-
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.stereotype.Service;
-@Service
-public class EndpointConfiguration {
- private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
- private Map endpointStatuses = new ConcurrentHashMap<>();
- private Map> endpointGroups = new ConcurrentHashMap<>();
-
- public EndpointConfiguration() {
- init();
- processEnvironmentConfigs();
- }
-
- public void enableEndpoint(String endpoint) {
- endpointStatuses.put(endpoint, true);
- }
-
- public void disableEndpoint(String endpoint) {
- if(!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
- logger.info("Disabling {}", endpoint);
- endpointStatuses.put(endpoint, false);
- }
- }
-
- public boolean isEndpointEnabled(String endpoint) {
- if (endpoint.startsWith("/")) {
- endpoint = endpoint.substring(1);
- }
- return endpointStatuses.getOrDefault(endpoint, true);
- }
-
- public void addEndpointToGroup(String group, String endpoint) {
- endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint);
- }
-
- public void enableGroup(String group) {
- Set endpoints = endpointGroups.get(group);
- if (endpoints != null) {
- for (String endpoint : endpoints) {
- enableEndpoint(endpoint);
- }
- }
- }
-
- public void disableGroup(String group) {
- Set endpoints = endpointGroups.get(group);
- if (endpoints != null) {
- for (String endpoint : endpoints) {
- disableEndpoint(endpoint);
- }
- }
- }
-
- public void init() {
- // Adding endpoints to "PageOps" group
- addEndpointToGroup("PageOps", "remove-pages");
- addEndpointToGroup("PageOps", "merge-pdfs");
- addEndpointToGroup("PageOps", "split-pdfs");
- addEndpointToGroup("PageOps", "pdf-organizer");
- addEndpointToGroup("PageOps", "rotate-pdf");
- addEndpointToGroup("PageOps", "multi-page-layout");
- addEndpointToGroup("PageOps", "scale-pages");
-
- // Adding endpoints to "Convert" group
- addEndpointToGroup("Convert", "pdf-to-img");
- addEndpointToGroup("Convert", "img-to-pdf");
- addEndpointToGroup("Convert", "pdf-to-pdfa");
- addEndpointToGroup("Convert", "file-to-pdf");
- addEndpointToGroup("Convert", "xlsx-to-pdf");
- addEndpointToGroup("Convert", "pdf-to-word");
- addEndpointToGroup("Convert", "pdf-to-presentation");
- addEndpointToGroup("Convert", "pdf-to-text");
- addEndpointToGroup("Convert", "pdf-to-html");
- addEndpointToGroup("Convert", "pdf-to-xml");
-
- // Adding endpoints to "Security" group
- addEndpointToGroup("Security", "add-password");
- addEndpointToGroup("Security", "remove-password");
- addEndpointToGroup("Security", "change-permissions");
- addEndpointToGroup("Security", "add-watermark");
- addEndpointToGroup("Security", "cert-sign");
-
-
-
- // Adding endpoints to "Other" group
- addEndpointToGroup("Other", "ocr-pdf");
- addEndpointToGroup("Other", "add-image");
- addEndpointToGroup("Other", "compress-pdf");
- addEndpointToGroup("Other", "extract-images");
- addEndpointToGroup("Other", "change-metadata");
- addEndpointToGroup("Other", "extract-image-scans");
- addEndpointToGroup("Other", "sign");
- addEndpointToGroup("Other", "flatten");
- addEndpointToGroup("Other", "repair");
- addEndpointToGroup("Other", "remove-blanks");
- addEndpointToGroup("Other", "compare");
-
-
-
-
-
-
-
- //CLI
- addEndpointToGroup("CLI", "compress-pdf");
- addEndpointToGroup("CLI", "extract-image-scans");
- addEndpointToGroup("CLI", "remove-blanks");
- addEndpointToGroup("CLI", "repair");
- addEndpointToGroup("CLI", "pdf-to-pdfa");
- addEndpointToGroup("CLI", "file-to-pdf");
- addEndpointToGroup("CLI", "xlsx-to-pdf");
- addEndpointToGroup("CLI", "pdf-to-word");
- addEndpointToGroup("CLI", "pdf-to-presentation");
- addEndpointToGroup("CLI", "pdf-to-text");
- addEndpointToGroup("CLI", "pdf-to-html");
- addEndpointToGroup("CLI", "pdf-to-xml");
- addEndpointToGroup("CLI", "ocr-pdf");
-
- //python
- addEndpointToGroup("Python", "extract-image-scans");
- addEndpointToGroup("Python", "remove-blanks");
-
-
-
- //openCV
- addEndpointToGroup("OpenCV", "extract-image-scans");
- addEndpointToGroup("OpenCV", "remove-blanks");
-
- //LibreOffice
- addEndpointToGroup("LibreOffice", "repair");
- addEndpointToGroup("LibreOffice", "file-to-pdf");
- addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
- addEndpointToGroup("LibreOffice", "pdf-to-word");
- addEndpointToGroup("LibreOffice", "pdf-to-presentation");
- addEndpointToGroup("LibreOffice", "pdf-to-text");
- addEndpointToGroup("LibreOffice", "pdf-to-html");
- addEndpointToGroup("LibreOffice", "pdf-to-xml");
-
-
- //OCRmyPDF
- addEndpointToGroup("OCRmyPDF", "compress-pdf");
- addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
- addEndpointToGroup("OCRmyPDF", "ocr-pdf");
-
- //Java
- addEndpointToGroup("Java", "merge-pdfs");
- addEndpointToGroup("Java", "remove-pages");
- addEndpointToGroup("Java", "split-pdfs");
- addEndpointToGroup("Java", "pdf-organizer");
- addEndpointToGroup("Java", "rotate-pdf");
- addEndpointToGroup("Java", "pdf-to-img");
- addEndpointToGroup("Java", "img-to-pdf");
- addEndpointToGroup("Java", "add-password");
- addEndpointToGroup("Java", "remove-password");
- addEndpointToGroup("Java", "change-permissions");
- addEndpointToGroup("Java", "add-watermark");
- addEndpointToGroup("Java", "add-image");
- addEndpointToGroup("Java", "extract-images");
- addEndpointToGroup("Java", "change-metadata");
- addEndpointToGroup("Java", "cert-sign");
- addEndpointToGroup("Java", "multi-page-layout");
- addEndpointToGroup("Java", "scale-pages");
-
-
- //Javascript
- addEndpointToGroup("Javascript", "pdf-organizer");
- addEndpointToGroup("Javascript", "sign");
- addEndpointToGroup("Javascript", "compare");
-
- }
-
- private void processEnvironmentConfigs() {
- String endpointsToRemove = System.getenv("ENDPOINTS_TO_REMOVE");
- String groupsToRemove = System.getenv("GROUPS_TO_REMOVE");
-
- if (endpointsToRemove != null) {
- String[] endpoints = endpointsToRemove.split(",");
- for (String endpoint : endpoints) {
- disableEndpoint(endpoint.trim());
- }
- }
-
- if (groupsToRemove != null) {
- String[] groups = groupsToRemove.split(",");
- for (String group : groups) {
- disableGroup(group.trim());
- }
- }
- }
-
-}
-
+package stirling.software.SPDF.config;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+@Service
+public class EndpointConfiguration {
+ private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
+ private Map endpointStatuses = new ConcurrentHashMap<>();
+ private Map> endpointGroups = new ConcurrentHashMap<>();
+
+ public EndpointConfiguration() {
+ init();
+ processEnvironmentConfigs();
+ }
+
+ public void enableEndpoint(String endpoint) {
+ endpointStatuses.put(endpoint, true);
+ }
+
+ public void disableEndpoint(String endpoint) {
+ if(!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
+ logger.info("Disabling {}", endpoint);
+ endpointStatuses.put(endpoint, false);
+ }
+ }
+
+ public boolean isEndpointEnabled(String endpoint) {
+ if (endpoint.startsWith("/")) {
+ endpoint = endpoint.substring(1);
+ }
+ return endpointStatuses.getOrDefault(endpoint, true);
+ }
+
+ public void addEndpointToGroup(String group, String endpoint) {
+ endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint);
+ }
+
+ public void enableGroup(String group) {
+ Set endpoints = endpointGroups.get(group);
+ if (endpoints != null) {
+ for (String endpoint : endpoints) {
+ enableEndpoint(endpoint);
+ }
+ }
+ }
+
+ public void disableGroup(String group) {
+ Set endpoints = endpointGroups.get(group);
+ if (endpoints != null) {
+ for (String endpoint : endpoints) {
+ disableEndpoint(endpoint);
+ }
+ }
+ }
+
+ public void init() {
+ // Adding endpoints to "PageOps" group
+ addEndpointToGroup("PageOps", "remove-pages");
+ addEndpointToGroup("PageOps", "merge-pdfs");
+ addEndpointToGroup("PageOps", "split-pdfs");
+ addEndpointToGroup("PageOps", "pdf-organizer");
+ addEndpointToGroup("PageOps", "rotate-pdf");
+ addEndpointToGroup("PageOps", "multi-page-layout");
+ addEndpointToGroup("PageOps", "scale-pages");
+ addEndpointToGroup("PageOps", "adjust-contrast");
+ addEndpointToGroup("PageOps", "crop");
+ addEndpointToGroup("PageOps", "auto-split-pdf");
+
+ // Adding endpoints to "Convert" group
+ addEndpointToGroup("Convert", "pdf-to-img");
+ addEndpointToGroup("Convert", "img-to-pdf");
+ addEndpointToGroup("Convert", "pdf-to-pdfa");
+ addEndpointToGroup("Convert", "file-to-pdf");
+ addEndpointToGroup("Convert", "xlsx-to-pdf");
+ addEndpointToGroup("Convert", "pdf-to-word");
+ addEndpointToGroup("Convert", "pdf-to-presentation");
+ addEndpointToGroup("Convert", "pdf-to-text");
+ addEndpointToGroup("Convert", "pdf-to-html");
+ addEndpointToGroup("Convert", "pdf-to-xml");
+
+ // Adding endpoints to "Security" group
+ addEndpointToGroup("Security", "add-password");
+ addEndpointToGroup("Security", "remove-password");
+ addEndpointToGroup("Security", "change-permissions");
+ addEndpointToGroup("Security", "add-watermark");
+ addEndpointToGroup("Security", "cert-sign");
+ addEndpointToGroup("Security", "sanitize-pdf");
+
+
+ // Adding endpoints to "Other" group
+ addEndpointToGroup("Other", "ocr-pdf");
+ addEndpointToGroup("Other", "add-image");
+ addEndpointToGroup("Other", "compress-pdf");
+ addEndpointToGroup("Other", "extract-images");
+ addEndpointToGroup("Other", "change-metadata");
+ addEndpointToGroup("Other", "extract-image-scans");
+ addEndpointToGroup("Other", "sign");
+ addEndpointToGroup("Other", "flatten");
+ addEndpointToGroup("Other", "repair");
+ addEndpointToGroup("Other", "remove-blanks");
+ addEndpointToGroup("Other", "compare");
+ addEndpointToGroup("Other", "add-page-numbers");
+ addEndpointToGroup("Other", "auto-rename");
+
+
+
+
+ //CLI
+ addEndpointToGroup("CLI", "compress-pdf");
+ addEndpointToGroup("CLI", "extract-image-scans");
+ addEndpointToGroup("CLI", "remove-blanks");
+ addEndpointToGroup("CLI", "repair");
+ addEndpointToGroup("CLI", "pdf-to-pdfa");
+ addEndpointToGroup("CLI", "file-to-pdf");
+ addEndpointToGroup("CLI", "xlsx-to-pdf");
+ addEndpointToGroup("CLI", "pdf-to-word");
+ addEndpointToGroup("CLI", "pdf-to-presentation");
+ addEndpointToGroup("CLI", "pdf-to-text");
+ addEndpointToGroup("CLI", "pdf-to-html");
+ addEndpointToGroup("CLI", "pdf-to-xml");
+ addEndpointToGroup("CLI", "ocr-pdf");
+
+ //python
+ addEndpointToGroup("Python", "extract-image-scans");
+ addEndpointToGroup("Python", "remove-blanks");
+
+
+
+ //openCV
+ addEndpointToGroup("OpenCV", "extract-image-scans");
+ addEndpointToGroup("OpenCV", "remove-blanks");
+
+ //LibreOffice
+ addEndpointToGroup("LibreOffice", "repair");
+ addEndpointToGroup("LibreOffice", "file-to-pdf");
+ addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
+ addEndpointToGroup("LibreOffice", "pdf-to-word");
+ addEndpointToGroup("LibreOffice", "pdf-to-presentation");
+ addEndpointToGroup("LibreOffice", "pdf-to-text");
+ addEndpointToGroup("LibreOffice", "pdf-to-html");
+ addEndpointToGroup("LibreOffice", "pdf-to-xml");
+
+
+ //OCRmyPDF
+ addEndpointToGroup("OCRmyPDF", "compress-pdf");
+ addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
+ addEndpointToGroup("OCRmyPDF", "ocr-pdf");
+
+ //Java
+ addEndpointToGroup("Java", "merge-pdfs");
+ addEndpointToGroup("Java", "remove-pages");
+ addEndpointToGroup("Java", "split-pdfs");
+ addEndpointToGroup("Java", "pdf-organizer");
+ addEndpointToGroup("Java", "rotate-pdf");
+ addEndpointToGroup("Java", "pdf-to-img");
+ addEndpointToGroup("Java", "img-to-pdf");
+ addEndpointToGroup("Java", "add-password");
+ addEndpointToGroup("Java", "remove-password");
+ addEndpointToGroup("Java", "change-permissions");
+ addEndpointToGroup("Java", "add-watermark");
+ addEndpointToGroup("Java", "add-image");
+ addEndpointToGroup("Java", "extract-images");
+ addEndpointToGroup("Java", "change-metadata");
+ addEndpointToGroup("Java", "cert-sign");
+ addEndpointToGroup("Java", "multi-page-layout");
+ addEndpointToGroup("Java", "scale-pages");
+ addEndpointToGroup("Java", "add-page-numbers");
+ addEndpointToGroup("Java", "auto-rename");
+ addEndpointToGroup("Java", "auto-split-pdf");
+ addEndpointToGroup("Java", "sanitize-pdf");
+ addEndpointToGroup("Java", "crop");
+
+ //Javascript
+ addEndpointToGroup("Javascript", "pdf-organizer");
+ addEndpointToGroup("Javascript", "sign");
+ addEndpointToGroup("Javascript", "compare");
+ addEndpointToGroup("Javascript", "adjust-contrast");
+
+
+ }
+
+ private void processEnvironmentConfigs() {
+ String endpointsToRemove = System.getenv("ENDPOINTS_TO_REMOVE");
+ String groupsToRemove = System.getenv("GROUPS_TO_REMOVE");
+
+ if (endpointsToRemove != null) {
+ String[] endpoints = endpointsToRemove.split(",");
+ for (String endpoint : endpoints) {
+ disableEndpoint(endpoint.trim());
+ }
+ }
+
+ if (groupsToRemove != null) {
+ String[] groups = groupsToRemove.split(",");
+ for (String group : groups) {
+ disableGroup(group.trim());
+ }
+ }
+ }
+
+}
+
diff --git a/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java b/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java
index 610b11c0..10a88e97 100644
--- a/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java
+++ b/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java
@@ -3,6 +3,7 @@ package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@@ -15,4 +16,12 @@ public class WebMvcConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(endpointInterceptor);
}
+
+ @Override
+ public void addResourceHandlers(ResourceHandlerRegistry registry) {
+ // Handler for external static resources
+ registry.addResourceHandler("/**")
+ .addResourceLocations("file:customFiles/static/", "classpath:/static/")
+ .setCachePeriod(0); // Optional: disable caching
+ }
}
diff --git a/src/main/java/stirling/software/SPDF/controller/api/CropController.java b/src/main/java/stirling/software/SPDF/controller/api/CropController.java
new file mode 100644
index 00000000..e56c1b40
--- /dev/null
+++ b/src/main/java/stirling/software/SPDF/controller/api/CropController.java
@@ -0,0 +1,132 @@
+package stirling.software.SPDF.controller.api;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RequestPart;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import stirling.software.SPDF.utils.GeneralUtils;
+import stirling.software.SPDF.utils.WebResponseUtils;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.multipart.MultipartFile;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import com.itextpdf.kernel.geom.PageSize;
+import com.itextpdf.kernel.geom.Rectangle;
+import com.itextpdf.kernel.pdf.PdfDocument;
+import com.itextpdf.kernel.pdf.PdfPage;
+import com.itextpdf.kernel.pdf.PdfReader;
+import com.itextpdf.kernel.pdf.PdfWriter;
+import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
+import com.itextpdf.kernel.pdf.canvas.parser.EventType;
+import com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor;
+import com.itextpdf.kernel.pdf.canvas.parser.data.IEventData;
+import com.itextpdf.kernel.pdf.canvas.parser.data.TextRenderInfo;
+import com.itextpdf.kernel.pdf.canvas.parser.listener.IEventListener;
+import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
+
+import io.swagger.v3.oas.annotations.Hidden;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import stirling.software.SPDF.utils.WebResponseUtils;
+
+@RestController
+@Tag(name = "General", description = "General APIs")
+public class CropController {
+
+ private static final Logger logger = LoggerFactory.getLogger(CropController.class);
+
+
+ @PostMapping(value = "/crop", consumes = "multipart/form-data")
+ @Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")
+ public ResponseEntity cropPdf(
+ @Parameter(description = "The input PDF file", required = true) @RequestParam("file") MultipartFile file,
+ @Parameter(description = "The x-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("x") float x,
+ @Parameter(description = "The y-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("y") float y,
+ @Parameter(description = "The width of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("width") float width,
+ @Parameter(description = "The height of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("height") float height) throws IOException {
+ byte[] bytes = file.getBytes();
+ System.out.println("x=" + x + ", " + "y=" + y + ", " + "width=" + width + ", " +"height=" + height );
+ PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes));
+ PdfDocument pdfDoc = new PdfDocument(reader);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ PdfWriter writer = new PdfWriter(baos);
+ PdfDocument outputPdf = new PdfDocument(writer);
+
+ int totalPages = pdfDoc.getNumberOfPages();
+
+ for (int i = 1; i <= totalPages; i++) {
+ PdfPage page = outputPdf.addNewPage(new PageSize(width, height));
+ PdfCanvas pdfCanvas = new PdfCanvas(page);
+
+ PdfFormXObject formXObject = pdfDoc.getPage(i).copyAsFormXObject(outputPdf);
+
+ // Save the graphics state, apply the transformations, add the object, and then
+ // restore the graphics state
+ pdfCanvas.saveState();
+ pdfCanvas.rectangle(x, y, width, height);
+ pdfCanvas.clip();
+ pdfCanvas.addXObject(formXObject, -x, -y);
+ pdfCanvas.restoreState();
+ }
+
+
+ outputPdf.close();
+ byte[] pdfContent = baos.toByteArray();
+ pdfDoc.close();
+ return WebResponseUtils.bytesToWebResponse(pdfContent,
+ file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf");
+ }
+
+}
diff --git a/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java b/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java
index 420902cf..e3b4434a 100644
--- a/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java
+++ b/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java
@@ -52,11 +52,11 @@ public class ScalePagesController {
@Operation(summary = "Change the size of a PDF page/document", description = "This operation takes an input PDF file and the size to scale the pages to in the output PDF file. Input:PDF Output:PDF Type:SISO")
public ResponseEntity scalePages(
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
- @Parameter(description = "The scale of pages in the output PDF. Acceptable values are A0-A10, B0-B9, LETTER, TABLOID, LEDGER, LEGAL, EXECUTIVE.", required = true, schema = @Schema(type = "String", allowableValues = {
+ @Parameter(description = "The scale of pages in the output PDF. Acceptable values are A0-A10, B0-B9, LETTER, TABLOID, LEDGER, LEGAL, EXECUTIVE.", required = true, schema = @Schema(type = "string", allowableValues = {
"A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10", "B0", "B1", "B2", "B3", "B4",
"B5", "B6", "B7", "B8", "B9", "LETTER", "TABLOID", "LEDGER", "LEGAL",
"EXECUTIVE" })) @RequestParam("pageSize") String targetPageSize,
- @Parameter(description = "The scale of the content on the pages of the output PDF. Acceptable values are floats.", required = true, schema = @Schema(type = "float")) @RequestParam("scaleFactor") float scaleFactor)
+ @Parameter(description = "The scale of the content on the pages of the output PDF. Acceptable values are floats.", required = true, schema = @Schema(type = "integer")) @RequestParam("scaleFactor") float scaleFactor)
throws IOException {
Map sizeMap = new HashMap<>();
diff --git a/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java b/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java
index 13732ba3..3409e1d3 100644
--- a/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java
+++ b/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java
@@ -1,162 +1,202 @@
-package stirling.software.SPDF.controller.api.filters;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.apache.pdfbox.pdmodel.PDDocument;
-import org.apache.pdfbox.pdmodel.PDPage;
-import org.apache.pdfbox.pdmodel.common.PDRectangle;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestPart;
-import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.multipart.MultipartFile;
-
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import stirling.software.SPDF.utils.PdfUtils;
-import stirling.software.SPDF.utils.ProcessExecutor;
-import stirling.software.SPDF.utils.WebResponseUtils;
-
-@RestController
-@Tag(name = "Filter", description = "Filter APIs")
-public class FilterController {
-
- @PostMapping(consumes = "multipart/form-data", value = "/contains-text")
- @Operation(summary = "Checks if a PDF contains set text, returns true if does", description = "Input:PDF Output:Boolean Type:SISO")
- public Boolean containsText(
- @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile,
- @Parameter(description = "The text to check for", required = true) String text,
- @Parameter(description = "The page number to check for text on accepts 'All', ranges like '1-4'", required = false) String pageNumber)
- throws IOException, InterruptedException {
- PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
- return PdfUtils.hasText(pdfDocument, pageNumber);
- }
-
- @PostMapping(consumes = "multipart/form-data", value = "/contains-image")
- @Operation(summary = "Checks if a PDF contains an image", description = "Input:PDF Output:Boolean Type:SISO")
- public Boolean containsImage(
- @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile,
- @Parameter(description = "The page number to check for image on accepts 'All', ranges like '1-4'", required = false) String pageNumber)
- throws IOException, InterruptedException {
- PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
- return PdfUtils.hasImagesOnPage(null);
- }
-
- @PostMapping(consumes = "multipart/form-data", value = "/page-count")
- @Operation(summary = "Checks if a PDF is greater, less or equal to a setPageCount", description = "Input:PDF Output:Boolean Type:SISO")
- public Boolean pageCount(
- @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
- @Parameter(description = "Page Count", required = true) String pageCount,
- @Parameter(description = "Comparison type, accepts Greater, Equal, Less than", required = false) String comparator)
- throws IOException, InterruptedException {
- // Load the PDF
- PDDocument document = PDDocument.load(inputFile.getInputStream());
- int actualPageCount = document.getNumberOfPages();
-
- // Perform the comparison
- switch (comparator) {
- case "Greater":
- return actualPageCount > Integer.parseInt(pageCount);
- case "Equal":
- return actualPageCount == Integer.parseInt(pageCount);
- case "Less":
- return actualPageCount < Integer.parseInt(pageCount);
- default:
- throw new IllegalArgumentException("Invalid comparator: " + comparator);
- }
- }
-
- @PostMapping(consumes = "multipart/form-data", value = "/page-size")
- @Operation(summary = "Checks if a PDF is of a certain size", description = "Input:PDF Output:Boolean Type:SISO")
- public Boolean pageSize(
- @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
- @Parameter(description = "Standard Page Size", required = true) String standardPageSize,
- @Parameter(description = "Comparison type, accepts Greater, Equal, Less than", required = false) String comparator)
- throws IOException, InterruptedException {
-
- // Load the PDF
- PDDocument document = PDDocument.load(inputFile.getInputStream());
-
- PDPage firstPage = document.getPage(0);
- PDRectangle actualPageSize = firstPage.getMediaBox();
-
- // Calculate the area of the actual page size
- float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight();
-
- // Get the standard size and calculate its area
- PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize);
- float standardArea = standardSize.getWidth() * standardSize.getHeight();
-
- // Perform the comparison
- switch (comparator) {
- case "Greater":
- return actualArea > standardArea;
- case "Equal":
- return actualArea == standardArea;
- case "Less":
- return actualArea < standardArea;
- default:
- throw new IllegalArgumentException("Invalid comparator: " + comparator);
- }
- }
-
-
- @PostMapping(consumes = "multipart/form-data", value = "/file-size")
- @Operation(summary = "Checks if a PDF is a set file size", description = "Input:PDF Output:Boolean Type:SISO")
- public Boolean fileSize(
- @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
- @Parameter(description = "File Size", required = true) String fileSize,
- @Parameter(description = "Comparison type, accepts Greater, Equal, Less than", required = false) String comparator)
- throws IOException, InterruptedException {
-
- // Get the file size
- long actualFileSize = inputFile.getSize();
-
- // Perform the comparison
- switch (comparator) {
- case "Greater":
- return actualFileSize > Long.parseLong(fileSize);
- case "Equal":
- return actualFileSize == Long.parseLong(fileSize);
- case "Less":
- return actualFileSize < Long.parseLong(fileSize);
- default:
- throw new IllegalArgumentException("Invalid comparator: " + comparator);
- }
- }
-
-
- @PostMapping(consumes = "multipart/form-data", value = "/page-rotation")
- @Operation(summary = "Checks if a PDF is of a certain rotation", description = "Input:PDF Output:Boolean Type:SISO")
- public Boolean pageRotation(
- @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
- @Parameter(description = "Rotation in degrees", required = true) int rotation,
- @Parameter(description = "Comparison type, accepts Greater, Equal, Less than", required = false) String comparator)
- throws IOException, InterruptedException {
-
- // Load the PDF
- PDDocument document = PDDocument.load(inputFile.getInputStream());
-
- // Get the rotation of the first page
- PDPage firstPage = document.getPage(0);
- int actualRotation = firstPage.getRotation();
-
- // Perform the comparison
- switch (comparator) {
- case "Greater":
- return actualRotation > rotation;
- case "Equal":
- return actualRotation == rotation;
- case "Less":
- return actualRotation < rotation;
- default:
- throw new IllegalArgumentException("Invalid comparator: " + comparator);
- }
- }
-
-}
+package stirling.software.SPDF.controller.api.filters;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.apache.pdfbox.pdmodel.common.PDRectangle;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestPart;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import stirling.software.SPDF.utils.PdfUtils;
+import stirling.software.SPDF.utils.ProcessExecutor;
+import stirling.software.SPDF.utils.WebResponseUtils;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+@RestController
+@Tag(name = "Filter", description = "Filter APIs")
+public class FilterController {
+
+ @PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text")
+ @Operation(summary = "Checks if a PDF contains set text, returns true if does", description = "Input:PDF Output:Boolean Type:SISO")
+ public ResponseEntity containsText(
+ @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile,
+ @Parameter(description = "The text to check for", required = true) String text,
+ @Parameter(description = "The page number to check for text on accepts 'All', ranges like '1-4'", required = false) String pageNumber)
+ throws IOException, InterruptedException {
+ PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
+ if (PdfUtils.hasText(pdfDocument, pageNumber, text))
+ return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename());
+ return null;
+ }
+
+ // TODO
+ @PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image")
+ @Operation(summary = "Checks if a PDF contains an image", description = "Input:PDF Output:Boolean Type:SISO")
+ public ResponseEntity containsImage(
+ @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile,
+ @Parameter(description = "The page number to check for image on accepts 'All', ranges like '1-4'", required = false) String pageNumber)
+ throws IOException, InterruptedException {
+ PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream());
+ if (PdfUtils.hasImages(pdfDocument, pageNumber))
+ return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename());
+ return null;
+ }
+
+ @PostMapping(consumes = "multipart/form-data", value = "/filter-page-count")
+ @Operation(summary = "Checks if a PDF is greater, less or equal to a setPageCount", description = "Input:PDF Output:Boolean Type:SISO")
+ public ResponseEntity pageCount(
+ @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
+ @Parameter(description = "Page Count", required = true) String pageCount,
+ @Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
+ "Greater", "Equal", "Less" })) String comparator)
+ throws IOException, InterruptedException {
+ // Load the PDF
+ PDDocument document = PDDocument.load(inputFile.getInputStream());
+ int actualPageCount = document.getNumberOfPages();
+
+ boolean valid = false;
+ // Perform the comparison
+ switch (comparator) {
+ case "Greater":
+ valid = actualPageCount > Integer.parseInt(pageCount);
+ break;
+ case "Equal":
+ valid = actualPageCount == Integer.parseInt(pageCount);
+ break;
+ case "Less":
+ valid = actualPageCount < Integer.parseInt(pageCount);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid comparator: " + comparator);
+ }
+
+ if (valid)
+ return WebResponseUtils.multiPartFileToWebResponse(inputFile);
+ return null;
+ }
+
+ @PostMapping(consumes = "multipart/form-data", value = "/filter-page-size")
+ @Operation(summary = "Checks if a PDF is of a certain size", description = "Input:PDF Output:Boolean Type:SISO")
+ public ResponseEntity pageSize(
+ @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
+ @Parameter(description = "Standard Page Size", required = true) String standardPageSize,
+ @Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
+ "Greater", "Equal", "Less" })) String comparator)
+ throws IOException, InterruptedException {
+
+ // Load the PDF
+ PDDocument document = PDDocument.load(inputFile.getInputStream());
+
+ PDPage firstPage = document.getPage(0);
+ PDRectangle actualPageSize = firstPage.getMediaBox();
+
+ // Calculate the area of the actual page size
+ float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight();
+
+ // Get the standard size and calculate its area
+ PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize);
+ float standardArea = standardSize.getWidth() * standardSize.getHeight();
+
+ boolean valid = false;
+ // Perform the comparison
+ switch (comparator) {
+ case "Greater":
+ valid = actualArea > standardArea;
+ break;
+ case "Equal":
+ valid = actualArea == standardArea;
+ break;
+ case "Less":
+ valid = actualArea < standardArea;
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid comparator: " + comparator);
+ }
+
+ if (valid)
+ return WebResponseUtils.multiPartFileToWebResponse(inputFile);
+ return null;
+ }
+
+ @PostMapping(consumes = "multipart/form-data", value = "/filter-file-size")
+ @Operation(summary = "Checks if a PDF is a set file size", description = "Input:PDF Output:Boolean Type:SISO")
+ public ResponseEntity fileSize(
+ @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
+ @Parameter(description = "File Size", required = true) String fileSize,
+ @Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
+ "Greater", "Equal", "Less" })) String comparator)
+ throws IOException, InterruptedException {
+
+ // Get the file size
+ long actualFileSize = inputFile.getSize();
+
+ boolean valid = false;
+ // Perform the comparison
+ switch (comparator) {
+ case "Greater":
+ valid = actualFileSize > Long.parseLong(fileSize);
+ break;
+ case "Equal":
+ valid = actualFileSize == Long.parseLong(fileSize);
+ break;
+ case "Less":
+ valid = actualFileSize < Long.parseLong(fileSize);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid comparator: " + comparator);
+ }
+
+ if (valid)
+ return WebResponseUtils.multiPartFileToWebResponse(inputFile);
+ return null;
+ }
+
+ @PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation")
+ @Operation(summary = "Checks if a PDF is of a certain rotation", description = "Input:PDF Output:Boolean Type:SISO")
+ public ResponseEntity pageRotation(
+ @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile,
+ @Parameter(description = "Rotation in degrees", required = true) int rotation,
+ @Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = {
+ "Greater", "Equal", "Less" })) String comparator)
+ throws IOException, InterruptedException {
+
+ // Load the PDF
+ PDDocument document = PDDocument.load(inputFile.getInputStream());
+
+ // Get the rotation of the first page
+ PDPage firstPage = document.getPage(0);
+ int actualRotation = firstPage.getRotation();
+ boolean valid = false;
+ // Perform the comparison
+ switch (comparator) {
+ case "Greater":
+ valid = actualRotation > rotation;
+ break;
+ case "Equal":
+ valid = actualRotation == rotation;
+ break;
+ case "Less":
+ valid = actualRotation < rotation;
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid comparator: " + comparator);
+ }
+
+ if (valid)
+ return WebResponseUtils.multiPartFileToWebResponse(inputFile);
+ return null;
+
+ }
+
+}
diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/AutoRenameController.java b/src/main/java/stirling/software/SPDF/controller/api/other/AutoRenameController.java
new file mode 100644
index 00000000..66fd70a6
--- /dev/null
+++ b/src/main/java/stirling/software/SPDF/controller/api/other/AutoRenameController.java
@@ -0,0 +1,177 @@
+package stirling.software.SPDF.controller.api.other;
+
+import java.io.IOException;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RequestPart;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import stirling.software.SPDF.utils.GeneralUtils;
+import stirling.software.SPDF.utils.PdfUtils;
+import stirling.software.SPDF.utils.WebResponseUtils;
+import org.apache.pdfbox.pdmodel.*;
+import org.apache.pdfbox.pdmodel.common.*;
+import org.apache.pdfbox.pdmodel.PDPageContentStream.*;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.http.*;
+import org.springframework.web.multipart.MultipartFile;
+import io.swagger.v3.oas.annotations.*;
+import io.swagger.v3.oas.annotations.media.*;
+import io.swagger.v3.oas.annotations.parameters.*;
+import org.apache.pdfbox.pdmodel.font.PDType1Font;
+import org.apache.pdfbox.text.TextPosition;
+import org.apache.tomcat.util.http.ResponseUtil;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.List;
+import java.util.ArrayList;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.multipart.MultipartFile;
+
+import com.itextpdf.io.font.constants.StandardFonts;
+import com.itextpdf.kernel.font.PdfFont;
+import com.itextpdf.kernel.font.PdfFontFactory;
+import com.itextpdf.kernel.geom.Rectangle;
+import com.itextpdf.kernel.pdf.PdfReader;
+import com.itextpdf.kernel.pdf.PdfWriter;
+import com.itextpdf.kernel.pdf.PdfDocument;
+import com.itextpdf.kernel.pdf.PdfPage;
+import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
+import com.itextpdf.layout.Canvas;
+import com.itextpdf.layout.element.Paragraph;
+import com.itextpdf.layout.properties.TextAlignment;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.io.*;
+import org.apache.pdfbox.pdmodel.*;
+import org.apache.pdfbox.text.*;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import io.swagger.v3.oas.annotations.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.springframework.http.ResponseEntity;
+@RestController
+@Tag(name = "Other", description = "Other APIs")
+public class AutoRenameController {
+
+ private static final Logger logger = LoggerFactory.getLogger(AutoRenameController.class);
+
+ private static final float TITLE_FONT_SIZE_THRESHOLD = 20.0f;
+ private static final int LINE_LIMIT = 11;
+
+ @PostMapping(consumes = "multipart/form-data", value = "/auto-rename")
+ @Operation(summary = "Extract header from PDF file", description = "This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO")
+ public ResponseEntity extractHeader(
+ @RequestPart(value = "fileInput") @Parameter(description = "The input PDF file from which the header is to be extracted.", required = true) MultipartFile file,
+ @RequestParam(required = false, defaultValue = "false") @Parameter(description = "Flag indicating whether to use the first text as a fallback if no suitable title is found. Defaults to false.", required = false) Boolean useFirstTextAsFallback)
+ throws Exception {
+
+ PDDocument document = PDDocument.load(file.getInputStream());
+ PDFTextStripper reader = new PDFTextStripper() {
+ class LineInfo {
+ String text;
+ float fontSize;
+
+ LineInfo(String text, float fontSize) {
+ this.text = text;
+ this.fontSize = fontSize;
+ }
+ }
+
+ List lineInfos = new ArrayList<>();
+ StringBuilder lineBuilder = new StringBuilder();
+ float lastY = -1;
+ float maxFontSizeInLine = 0.0f;
+ int lineCount = 0;
+
+ @Override
+ protected void processTextPosition(TextPosition text) {
+ if (lastY != text.getY() && lineCount < LINE_LIMIT) {
+ processLine();
+ lineBuilder = new StringBuilder(text.getUnicode());
+ maxFontSizeInLine = text.getFontSizeInPt();
+ lastY = text.getY();
+ lineCount++;
+ } else if (lineCount < LINE_LIMIT) {
+ lineBuilder.append(text.getUnicode());
+ if (text.getFontSizeInPt() > maxFontSizeInLine) {
+ maxFontSizeInLine = text.getFontSizeInPt();
+ }
+ }
+ }
+
+ private void processLine() {
+ if (lineBuilder.length() > 0 && lineCount < LINE_LIMIT) {
+ lineInfos.add(new LineInfo(lineBuilder.toString(), maxFontSizeInLine));
+ }
+ }
+
+ @Override
+ public String getText(PDDocument doc) throws IOException {
+ this.lineInfos.clear();
+ this.lineBuilder = new StringBuilder();
+ this.lastY = -1;
+ this.maxFontSizeInLine = 0.0f;
+ this.lineCount = 0;
+ super.getText(doc);
+ processLine(); // Process the last line
+
+ // Merge lines with same font size
+ List mergedLineInfos = new ArrayList<>();
+ for (int i = 0; i < lineInfos.size(); i++) {
+ String mergedText = lineInfos.get(i).text;
+ float fontSize = lineInfos.get(i).fontSize;
+ while (i + 1 < lineInfos.size() && lineInfos.get(i + 1).fontSize == fontSize) {
+ mergedText += " " + lineInfos.get(i + 1).text;
+ i++;
+ }
+ mergedLineInfos.add(new LineInfo(mergedText, fontSize));
+ }
+
+ // Sort lines by font size in descending order and get the first one
+ mergedLineInfos.sort(Comparator.comparing((LineInfo li) -> li.fontSize).reversed());
+ String title = mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(0).text;
+
+ return title != null ? title : (useFirstTextAsFallback ? (mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(mergedLineInfos.size() - 1).text) : null);
+ }
+
+ };
+
+ String header = reader.getText(document);
+
+
+
+ // Sanitize the header string by removing characters not allowed in a filename.
+ if (header != null && header.length() < 255) {
+ header = header.replaceAll("[/\\\\?%*:|\"<>]", "");
+ return WebResponseUtils.pdfDocToWebResponse(document, header + ".pdf");
+ } else {
+ logger.info("File has no good title to be found");
+ return WebResponseUtils.pdfDocToWebResponse(document, file.getOriginalFilename());
+ }
+ }
+
+
+
+
+}
diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/AutoSplitPdfController.java b/src/main/java/stirling/software/SPDF/controller/api/other/AutoSplitPdfController.java
new file mode 100644
index 00000000..e69c0597
--- /dev/null
+++ b/src/main/java/stirling/software/SPDF/controller/api/other/AutoSplitPdfController.java
@@ -0,0 +1,137 @@
+package stirling.software.SPDF.controller.api.other;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferByte;
+import java.awt.image.DataBufferInt;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.rendering.PDFRenderer;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import com.google.zxing.BinaryBitmap;
+import com.google.zxing.LuminanceSource;
+import com.google.zxing.MultiFormatReader;
+import com.google.zxing.NotFoundException;
+import com.google.zxing.PlanarYUVLuminanceSource;
+import com.google.zxing.Result;
+import com.google.zxing.common.HybridBinarizer;
+
+import stirling.software.SPDF.utils.WebResponseUtils;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+
+@RestController
+public class AutoSplitPdfController {
+
+ private static final String QR_CONTENT = "https://github.com/Frooodle/Stirling-PDF";
+
+ @PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data")
+ @Operation(summary = "Auto split PDF pages into separate documents", description = "This endpoint accepts a PDF file, scans each page for a specific QR code, and splits the document at the QR code boundaries. The output is a zip file containing each separate PDF document. Input:PDF Output:ZIP Type:SISO")
+ public ResponseEntity autoSplitPdf(
+ @RequestParam("fileInput") @Parameter(description = "The input PDF file which needs to be split into separate documents based on QR code boundaries.", required = true) MultipartFile file)
+ throws IOException {
+ InputStream inputStream = file.getInputStream();
+ PDDocument document = PDDocument.load(inputStream);
+ PDFRenderer pdfRenderer = new PDFRenderer(document);
+
+ List splitDocuments = new ArrayList<>();
+ List splitDocumentsBoas = new ArrayList<>(); // create this list to store ByteArrayOutputStreams for zipping
+
+ for (int page = 0; page < document.getNumberOfPages(); ++page) {
+ BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 150);
+ String result = decodeQRCode(bim);
+
+ if(QR_CONTENT.equals(result) && page != 0) {
+ splitDocuments.add(new PDDocument());
+ }
+
+ if (!splitDocuments.isEmpty() && !QR_CONTENT.equals(result)) {
+ splitDocuments.get(splitDocuments.size() - 1).addPage(document.getPage(page));
+ } else if (page == 0) {
+ PDDocument firstDocument = new PDDocument();
+ firstDocument.addPage(document.getPage(page));
+ splitDocuments.add(firstDocument);
+ }
+ }
+
+ // After all pages are added to splitDocuments, convert each to ByteArrayOutputStream and add to splitDocumentsBoas
+ for (PDDocument splitDocument : splitDocuments) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ splitDocument.save(baos);
+ splitDocumentsBoas.add(baos);
+ splitDocument.close();
+ }
+
+ document.close();
+
+ // After this line, you can find your zip logic integrated
+ Path zipFile = Files.createTempFile("split_documents", ".zip");
+ String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
+ byte[] data;
+ try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
+ // loop through the split documents and write them to the zip file
+ for (int i = 0; i < splitDocumentsBoas.size(); i++) {
+ String fileName = filename + "_" + (i + 1) + ".pdf"; // You should replace "originalFileName" with the real file name
+ ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
+ byte[] pdf = baos.toByteArray();
+
+ // Add PDF file to the zip
+ ZipEntry pdfEntry = new ZipEntry(fileName);
+ zipOut.putNextEntry(pdfEntry);
+ zipOut.write(pdf);
+ zipOut.closeEntry();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ data = Files.readAllBytes(zipFile);
+ Files.delete(zipFile);
+ }
+
+
+
+ // return the Resource in the response
+ return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
+ }
+
+
+ private static String decodeQRCode(BufferedImage bufferedImage) {
+ LuminanceSource source;
+
+ if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte) {
+ byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData();
+ source = new PlanarYUVLuminanceSource(pixels, bufferedImage.getWidth(), bufferedImage.getHeight(), 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight(), false);
+ } else if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferInt) {
+ int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
+ byte[] newPixels = new byte[pixels.length];
+ for (int i = 0; i < pixels.length; i++) {
+ newPixels[i] = (byte) (pixels[i] & 0xff);
+ }
+ source = new PlanarYUVLuminanceSource(newPixels, bufferedImage.getWidth(), bufferedImage.getHeight(), 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight(), false);
+ } else {
+ throw new IllegalArgumentException("BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed int), byte gray, or 3-byte/4-byte RGB image data");
+ }
+
+ BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
+
+ try {
+ Result result = new MultiFormatReader().decode(bitmap);
+ return result.getText();
+ } catch (NotFoundException e) {
+ return null; // there is no QR code in the image
+ }
+ }
+}
diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/CompressController.java b/src/main/java/stirling/software/SPDF/controller/api/other/CompressController.java
index 0002b808..42ab6a41 100644
--- a/src/main/java/stirling/software/SPDF/controller/api/other/CompressController.java
+++ b/src/main/java/stirling/software/SPDF/controller/api/other/CompressController.java
@@ -221,6 +221,15 @@ public class CompressController {
// Read the optimized PDF file
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
+ // Check if optimized file is larger than the original
+ if(pdfBytes.length > inputFileSize) {
+ // Log the occurrence
+ logger.warn("Optimized file is larger than the original. Returning the original file instead.");
+
+ // Read the original file again
+ pdfBytes = Files.readAllBytes(tempInputFile);
+ }
+
// Clean up the temporary files
Files.delete(tempInputFile);
Files.delete(tempOutputFile);
diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java b/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java
new file mode 100644
index 00000000..c1454a24
--- /dev/null
+++ b/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java
@@ -0,0 +1,176 @@
+package stirling.software.SPDF.controller.api.other;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RequestPart;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import stirling.software.SPDF.utils.GeneralUtils;
+import stirling.software.SPDF.utils.PdfUtils;
+import stirling.software.SPDF.utils.WebResponseUtils;
+import org.apache.pdfbox.pdmodel.*;
+import org.apache.pdfbox.pdmodel.common.*;
+import org.apache.pdfbox.pdmodel.PDPageContentStream.*;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.http.*;
+import org.springframework.web.multipart.MultipartFile;
+import io.swagger.v3.oas.annotations.*;
+import io.swagger.v3.oas.annotations.media.*;
+import io.swagger.v3.oas.annotations.parameters.*;
+import org.apache.pdfbox.pdmodel.font.PDType1Font;
+import org.apache.tomcat.util.http.ResponseUtil;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.List;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.multipart.MultipartFile;
+
+import com.itextpdf.io.font.constants.StandardFonts;
+import com.itextpdf.kernel.font.PdfFont;
+import com.itextpdf.kernel.font.PdfFontFactory;
+import com.itextpdf.kernel.geom.Rectangle;
+import com.itextpdf.kernel.pdf.PdfReader;
+import com.itextpdf.kernel.pdf.PdfWriter;
+import com.itextpdf.kernel.pdf.PdfDocument;
+import com.itextpdf.kernel.pdf.PdfPage;
+import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
+import com.itextpdf.layout.Canvas;
+import com.itextpdf.layout.element.Paragraph;
+import com.itextpdf.layout.properties.TextAlignment;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.io.*;
+
+@RestController
+@Tag(name = "Other", description = "Other APIs")
+public class PageNumbersController {
+
+ private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class);
+
+ @PostMapping(value = "/add-page-numbers", consumes = "multipart/form-data")
+ @Operation(summary = "Add page numbers to a PDF document", description = "This operation takes an input PDF file and adds page numbers to it. Input:PDF Output:PDF Type:SISO")
+ public ResponseEntity addPageNumbers(
+ @Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
+ @Parameter(description = "Custom margin: small/medium/large", required = true, schema = @Schema(type = "string", allowableValues = {"small", "medium", "large"})) @RequestParam("customMargin") String customMargin,
+ @Parameter(description = "Position: 1 of 9 positions", required = true, schema = @Schema(type = "integer", minimum = "1", maximum = "9")) @RequestParam("position") int position,
+ @Parameter(description = "Starting number", required = true, schema = @Schema(type = "integer", minimum = "1")) @RequestParam("startingNumber") int startingNumber,
+ @Parameter(description = "Which pages to number, default all", required = false, schema = @Schema(type = "string")) @RequestParam(value = "pagesToNumber", required = false) String pagesToNumber,
+ @Parameter(description = "Custom text: defaults to just number but can have things like \"Page {n} of {p}\"", required = false, schema = @Schema(type = "string")) @RequestParam(value = "customText", required = false) String customText)
+ throws IOException {
+
+ byte[] fileBytes = file.getBytes();
+ ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
+
+ int pageNumber = startingNumber;
+ float marginFactor;
+ switch (customMargin.toLowerCase()) {
+ case "small":
+ marginFactor = 0.02f;
+ break;
+ case "medium":
+ marginFactor = 0.035f;
+ break;
+ case "large":
+ marginFactor = 0.05f;
+ break;
+ case "x-large":
+ marginFactor = 0.1f;
+ break;
+ default:
+ marginFactor = 0.035f;
+ break;
+ }
+
+ float fontSize = 12.0f;
+
+ PdfReader reader = new PdfReader(bais);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ PdfWriter writer = new PdfWriter(baos);
+
+ PdfDocument pdfDoc = new PdfDocument(reader, writer);
+
+ List pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), pdfDoc.getNumberOfPages());
+
+ for (int i : pagesToNumberList) {
+ PdfPage page = pdfDoc.getPage(i+1);
+ Rectangle pageSize = page.getPageSize();
+ PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamAfter(), page.getResources(), pdfDoc);
+
+ String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(pdfDoc.getNumberOfPages())) : String.valueOf(pageNumber);
+
+ PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA);
+ float textWidth = font.getWidth(text, fontSize);
+ float textHeight = font.getAscent(text, fontSize) - font.getDescent(text, fontSize);
+
+ float x, y;
+ TextAlignment alignment;
+
+ int xGroup = (position - 1) % 3;
+ int yGroup = 2 - (position - 1) / 3;
+
+ switch (xGroup) {
+ case 0: // left
+ x = pageSize.getLeft() + marginFactor * pageSize.getWidth();
+ alignment = TextAlignment.LEFT;
+ break;
+ case 1: // center
+ x = pageSize.getLeft() + (pageSize.getWidth()) / 2;
+ alignment = TextAlignment.CENTER;
+ break;
+ default: // right
+ x = pageSize.getRight() - marginFactor * pageSize.getWidth();
+ alignment = TextAlignment.RIGHT;
+ break;
+ }
+
+ switch (yGroup) {
+ case 0: // bottom
+ y = pageSize.getBottom() + marginFactor * pageSize.getHeight();
+ break;
+ case 1: // middle
+ y = pageSize.getBottom() + (pageSize.getHeight() ) / 2;
+ break;
+ default: // top
+ y = pageSize.getTop() - marginFactor * pageSize.getHeight();
+ break;
+ }
+
+ new Canvas(pdfCanvas, page.getPageSize())
+ .showTextAligned(new Paragraph(text).setFont(font).setFontSize(fontSize), x, y, alignment);
+
+ pageNumber++;
+ }
+
+
+ pdfDoc.close();
+ byte[] resultBytes = baos.toByteArray();
+
+ return ResponseEntity.ok()
+ .header("Content-Type", "application/pdf; charset=UTF-8")
+ .header("Content-Disposition", "inline; filename=" + URLEncoder.encode(file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", "UTF-8"))
+ .body(resultBytes);
+ }
+
+
+
+}
diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/Controller.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/Controller.java
deleted file mode 100644
index d0325450..00000000
--- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/Controller.java
+++ /dev/null
@@ -1,399 +0,0 @@
-package stirling.software.SPDF.controller.api.pipeline;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PrintStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.time.LocalDate;
-import java.time.LocalTime;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Stream;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
-import java.util.zip.ZipOutputStream;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.core.io.ByteArrayResource;
-import org.springframework.core.io.Resource;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.util.LinkedMultiValueMap;
-import org.springframework.util.MultiValueMap;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RequestPart;
-import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.client.RestTemplate;
-import org.springframework.web.multipart.MultipartFile;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import io.swagger.v3.oas.annotations.tags.Tag;
-import stirling.software.SPDF.model.PipelineConfig;
-import stirling.software.SPDF.model.PipelineOperation;
-import stirling.software.SPDF.utils.WebResponseUtils;
-
-
-@RestController
-@Tag(name = "Pipeline", description = "Pipeline APIs")
-public class Controller {
-
- @Autowired
- private ObjectMapper objectMapper;
-
-
- final String jsonFileName = "pipelineCofig.json";
- final String watchedFoldersDir = "watchedFolders/";
- @Scheduled(fixedRate = 5000)
- public void scanFolders() {
- Path watchedFolderPath = Paths.get(watchedFoldersDir);
- if (!Files.exists(watchedFolderPath)) {
- try {
- Files.createDirectories(watchedFolderPath);
- } catch (IOException e) {
- e.printStackTrace();
- return;
- }
- }
-
- try (Stream paths = Files.walk(watchedFolderPath)) {
- paths.filter(Files::isDirectory).forEach(t -> {
- try {
- if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) {
- handleDirectory(t);
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- });
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- private void handleDirectory(Path dir) throws Exception {
- Path jsonFile = dir.resolve(jsonFileName);
- Path processingDir = dir.resolve("processing"); // Directory to move files during processing
- if (!Files.exists(processingDir)) {
- Files.createDirectory(processingDir);
- }
-
- if (Files.exists(jsonFile)) {
- // Read JSON file
- String jsonString;
- try {
- jsonString = new String(Files.readAllBytes(jsonFile));
- } catch (IOException e) {
- e.printStackTrace();
- return;
- }
-
- // Decode JSON to PipelineConfig
- PipelineConfig config;
- try {
- config = objectMapper.readValue(jsonString, PipelineConfig.class);
- // Assuming your PipelineConfig class has getters for all necessary fields, you can perform checks here
- if (config.getOperations() == null || config.getOutputDir() == null || config.getName() == null) {
- throw new IOException("Invalid JSON format");
- }
- } catch (IOException e) {
- e.printStackTrace();
- return;
- }
-
- // For each operation in the pipeline
- for (PipelineOperation operation : config.getOperations()) {
- // Collect all files based on fileInput
- File[] files;
- String fileInput = (String) operation.getParameters().get("fileInput");
- if ("automated".equals(fileInput)) {
- // If fileInput is "automated", process all files in the directory
- try (Stream paths = Files.list(dir)) {
- files = paths.filter(path -> !path.equals(jsonFile))
- .map(Path::toFile)
- .toArray(File[]::new);
- } catch (IOException e) {
- e.printStackTrace();
- return;
- }
- } else {
- // If fileInput contains a path, process only this file
- files = new File[]{new File(fileInput)};
- }
-
- // Prepare the files for processing
- File[] filesToProcess = files.clone();
- for (File file : filesToProcess) {
- Files.move(file.toPath(), processingDir.resolve(file.getName()));
- }
-
- // Process the files
- try {
- List resources = handleFiles(filesToProcess, jsonString);
-
- // Move resultant files and rename them as per config in JSON file
- for (Resource resource : resources) {
- String outputFileName = config.getOutputPattern().replace("{filename}", resource.getFile().getName());
- outputFileName = outputFileName.replace("{pipelineName}", config.getName());
- DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
- outputFileName = outputFileName.replace("{date}", LocalDate.now().format(dateFormatter));
- DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HHmmss");
- outputFileName = outputFileName.replace("{time}", LocalTime.now().format(timeFormatter));
- // {filename} {folder} {date} {tmime} {pipeline}
-
- Files.move(resource.getFile().toPath(), Paths.get(config.getOutputDir(), outputFileName));
- }
-
- // If successful, delete the original files
- for (File file : filesToProcess) {
- Files.deleteIfExists(processingDir.resolve(file.getName()));
- }
- } catch (Exception e) {
- // If an error occurs, move the original files back
- for (File file : filesToProcess) {
- Files.move(processingDir.resolve(file.getName()), file.toPath());
- }
- throw e;
- }
- }
- }
- }
-
-
-
-
-List processFiles(List outputFiles, String jsonString) throws Exception{
- ObjectMapper mapper = new ObjectMapper();
- JsonNode jsonNode = mapper.readTree(jsonString);
-
- JsonNode pipelineNode = jsonNode.get("pipeline");
- ByteArrayOutputStream logStream = new ByteArrayOutputStream();
- PrintStream logPrintStream = new PrintStream(logStream);
-
- boolean hasErrors = false;
-
- for (JsonNode operationNode : pipelineNode) {
- String operation = operationNode.get("operation").asText();
- JsonNode parametersNode = operationNode.get("parameters");
- String inputFileExtension = "";
- if(operationNode.has("inputFileType")) {
- inputFileExtension = operationNode.get("inputFileType").asText();
- } else {
- inputFileExtension=".pdf";
- }
-
- List newOutputFiles = new ArrayList<>();
- boolean hasInputFileType = false;
-
- for (Resource file : outputFiles) {
- if (file.getFilename().endsWith(inputFileExtension)) {
- hasInputFileType = true;
- MultiValueMap body = new LinkedMultiValueMap<>();
- body.add("fileInput", file);
-
- Iterator> parameters = parametersNode.fields();
- while (parameters.hasNext()) {
- Map.Entry parameter = parameters.next();
- body.add(parameter.getKey(), parameter.getValue().asText());
- }
-
- HttpHeaders headers = new HttpHeaders();
- headers.setContentType(MediaType.MULTIPART_FORM_DATA);
-
- HttpEntity> entity = new HttpEntity<>(body, headers);
-
- RestTemplate restTemplate = new RestTemplate();
- String url = "http://localhost:8080/" + operation;
-
- ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
-
- if (!response.getStatusCode().equals(HttpStatus.OK)) {
- logPrintStream.println("Error: " + response.getBody());
- hasErrors = true;
- continue;
- }
-
- // Check if the response body is a zip file
- if (isZip(response.getBody())) {
- // Unzip the file and add all the files to the new output files
- newOutputFiles.addAll(unzip(response.getBody()));
- } else {
- Resource outputResource = new ByteArrayResource(response.getBody()) {
- @Override
- public String getFilename() {
- return file.getFilename(); // Preserving original filename
- }
- };
- newOutputFiles.add(outputResource);
- }
- }
-
- if (!hasInputFileType) {
- logPrintStream.println("No files with extension " + inputFileExtension + " found for operation " + operation);
- hasErrors = true;
- }
-
- outputFiles = newOutputFiles;
- }
- logPrintStream.close();
-
- }
- return outputFiles;
-}
-
-
-List handleFiles(File[] files, String jsonString) throws Exception{
- ObjectMapper mapper = new ObjectMapper();
- JsonNode jsonNode = mapper.readTree(jsonString);
-
- JsonNode pipelineNode = jsonNode.get("pipeline");
- ByteArrayOutputStream logStream = new ByteArrayOutputStream();
- PrintStream logPrintStream = new PrintStream(logStream);
-
- boolean hasErrors = false;
- List outputFiles = new ArrayList<>();
-
- for (File file : files) {
- Path path = Paths.get(file.getAbsolutePath());
- Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) {
- @Override
- public String getFilename() {
- return file.getName();
- }
- };
- outputFiles.add(fileResource);
- }
- return processFiles(outputFiles, jsonString);
-}
-
- List handleFiles(MultipartFile[] files, String jsonString) throws Exception{
- ObjectMapper mapper = new ObjectMapper();
- JsonNode jsonNode = mapper.readTree(jsonString);
-
- JsonNode pipelineNode = jsonNode.get("pipeline");
- ByteArrayOutputStream logStream = new ByteArrayOutputStream();
- PrintStream logPrintStream = new PrintStream(logStream);
-
- boolean hasErrors = false;
- List outputFiles = new ArrayList<>();
-
- for (MultipartFile file : files) {
- Resource fileResource = new ByteArrayResource(file.getBytes()) {
- @Override
- public String getFilename() {
- return file.getOriginalFilename();
- }
- };
- outputFiles.add(fileResource);
- }
- return processFiles(outputFiles, jsonString);
- }
-
- @PostMapping("/handleData")
- public ResponseEntity handleData(@RequestPart("fileInput") MultipartFile[] files,
- @RequestParam("json") String jsonString) {
- try {
-
- List outputFiles = handleFiles(files, jsonString);
-
- if (outputFiles.size() == 1) {
- // If there is only one file, return it directly
- Resource singleFile = outputFiles.get(0);
- InputStream is = singleFile.getInputStream();
- byte[] bytes = new byte[(int)singleFile.contentLength()];
- is.read(bytes);
- is.close();
-
- return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM);
- }
-
- // Create a ByteArrayOutputStream to hold the zip
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- ZipOutputStream zipOut = new ZipOutputStream(baos);
-
- // Loop through each file and add it to the zip
- for (Resource file : outputFiles) {
- ZipEntry zipEntry = new ZipEntry(file.getFilename());
- zipOut.putNextEntry(zipEntry);
-
- // Read the file into a byte array
- InputStream is = file.getInputStream();
- byte[] bytes = new byte[(int)file.contentLength()];
- is.read(bytes);
-
- // Write the bytes of the file to the zip
- zipOut.write(bytes, 0, bytes.length);
- zipOut.closeEntry();
-
- is.close();
- }
-
- zipOut.close();
-
- return WebResponseUtils.boasToWebResponse(baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
- } catch (Exception e) {
- e.printStackTrace();
- return null;
- }
- }
-
- private boolean isZip(byte[] data) {
- if (data == null || data.length < 4) {
- return false;
- }
-
- // Check the first four bytes of the data against the standard zip magic number
- return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04;
- }
-
- private List unzip(byte[] data) throws IOException {
- List unzippedFiles = new ArrayList<>();
-
- try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
- ZipInputStream zis = new ZipInputStream(bais)) {
-
- ZipEntry entry;
- while ((entry = zis.getNextEntry()) != null) {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- byte[] buffer = new byte[1024];
- int count;
-
- while ((count = zis.read(buffer)) != -1) {
- baos.write(buffer, 0, count);
- }
-
- final String filename = entry.getName();
- Resource fileResource = new ByteArrayResource(baos.toByteArray()) {
- @Override
- public String getFilename() {
- return filename;
- }
- };
-
- // If the unzipped file is a zip file, unzip it
- if (isZip(baos.toByteArray())) {
- unzippedFiles.addAll(unzip(baos.toByteArray()));
- } else {
- unzippedFiles.add(fileResource);
- }
- }
- }
-
- return unzippedFiles;
- }
-}
diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java
new file mode 100644
index 00000000..244cb807
--- /dev/null
+++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java
@@ -0,0 +1,516 @@
+package stirling.software.SPDF.controller.api.pipeline;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.Resource;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RequestPart;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.multipart.MultipartFile;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import io.swagger.v3.oas.annotations.tags.Tag;
+import stirling.software.SPDF.model.PipelineConfig;
+import stirling.software.SPDF.model.PipelineOperation;
+import stirling.software.SPDF.utils.WebResponseUtils;
+
+@RestController
+@Tag(name = "Pipeline", description = "Pipeline APIs")
+public class PipelineController {
+
+ private static final Logger logger = LoggerFactory.getLogger(PipelineController.class);
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ final String jsonFileName = "pipelineConfig.json";
+ final String watchedFoldersDir = "./pipeline/watchedFolders/";
+ final String finishedFoldersDir = "./pipeline/finishedFolders/";
+
+ @Scheduled(fixedRate = 25000)
+ public void scanFolders() {
+ logger.info("Scanning folders...");
+ Path watchedFolderPath = Paths.get(watchedFoldersDir);
+ if (!Files.exists(watchedFolderPath)) {
+ try {
+ Files.createDirectories(watchedFolderPath);
+ logger.info("Created directory: {}", watchedFolderPath);
+ } catch (IOException e) {
+ logger.error("Error creating directory: {}", watchedFolderPath, e);
+ return;
+ }
+ }
+ try (Stream paths = Files.walk(watchedFolderPath)) {
+ paths.filter(Files::isDirectory).forEach(t -> {
+ try {
+ if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) {
+ handleDirectory(t);
+ }
+ } catch (Exception e) {
+ logger.error("Error handling directory: {}", t, e);
+ }
+ });
+ } catch (Exception e) {
+ logger.error("Error walking through directory: {}", watchedFolderPath, e);
+ }
+ }
+
+ private void handleDirectory(Path dir) throws Exception {
+ logger.info("Handling directory: {}", dir);
+ Path jsonFile = dir.resolve(jsonFileName);
+ Path processingDir = dir.resolve("processing"); // Directory to move files during processing
+ if (!Files.exists(processingDir)) {
+ Files.createDirectory(processingDir);
+ logger.info("Created processing directory: {}", processingDir);
+ }
+
+ if (Files.exists(jsonFile)) {
+ // Read JSON file
+ String jsonString;
+ try {
+ jsonString = new String(Files.readAllBytes(jsonFile));
+ logger.info("Read JSON file: {}", jsonFile);
+ } catch (IOException e) {
+ logger.error("Error reading JSON file: {}", jsonFile, e);
+ return;
+ }
+
+ // Decode JSON to PipelineConfig
+ PipelineConfig config;
+ try {
+ config = objectMapper.readValue(jsonString, PipelineConfig.class);
+ // Assuming your PipelineConfig class has getters for all necessary fields, you
+ // can perform checks here
+ if (config.getOperations() == null || config.getOutputDir() == null || config.getName() == null) {
+ throw new IOException("Invalid JSON format");
+ }
+ } catch (IOException e) {
+ logger.error("Error parsing PipelineConfig: {}", jsonString, e);
+ return;
+ }
+
+ // For each operation in the pipeline
+ for (PipelineOperation operation : config.getOperations()) {
+ // Collect all files based on fileInput
+ File[] files;
+ String fileInput = (String) operation.getParameters().get("fileInput");
+ if ("automated".equals(fileInput)) {
+ // If fileInput is "automated", process all files in the directory
+ try (Stream paths = Files.list(dir)) {
+ files = paths
+ .filter(path -> !Files.isDirectory(path)) // exclude directories
+ .filter(path -> !path.equals(jsonFile)) // exclude jsonFile
+ .map(Path::toFile)
+ .toArray(File[]::new);
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ return;
+ }
+ } else {
+ // If fileInput contains a path, process only this file
+ files = new File[] { new File(fileInput) };
+ }
+
+ // Prepare the files for processing
+ List filesToProcess = new ArrayList<>();
+ for (File file : files) {
+ logger.info(file.getName());
+ logger.info("{} to {}",file.toPath(), processingDir.resolve(file.getName()));
+ Files.move(file.toPath(), processingDir.resolve(file.getName()));
+ filesToProcess.add(processingDir.resolve(file.getName()).toFile());
+ }
+
+ // Process the files
+ try {
+ List resources = handleFiles(filesToProcess.toArray(new File[0]), jsonString);
+
+ if(resources == null) {
+ return;
+ }
+ // Move resultant files and rename them as per config in JSON file
+ for (Resource resource : resources) {
+ String resourceName = resource.getFilename();
+ String baseName = resourceName.substring(0, resourceName.lastIndexOf("."));
+ String extension = resourceName.substring(resourceName.lastIndexOf(".")+1);
+
+ String outputFileName = config.getOutputPattern().replace("{filename}", baseName);
+
+ outputFileName = outputFileName.replace("{pipelineName}", config.getName());
+ DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
+ outputFileName = outputFileName.replace("{date}", LocalDate.now().format(dateFormatter));
+ DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HHmmss");
+ outputFileName = outputFileName.replace("{time}", LocalTime.now().format(timeFormatter));
+
+ outputFileName += "." + extension;
+ // {filename} {folder} {date} {tmime} {pipeline}
+ String outputDir = config.getOutputDir();
+
+ // Check if the environment variable 'automatedOutputFolder' is set
+ String outputFolder = System.getenv("automatedOutputFolder");
+
+ if (outputFolder == null || outputFolder.isEmpty()) {
+ // If the environment variable is not set, use the default value
+ outputFolder = finishedFoldersDir;
+ }
+ logger.info("outputDir 0={}", outputDir);
+ // Replace the placeholders in the outputDir string
+ outputDir = outputDir.replace("{outputFolder}", outputFolder);
+ outputDir = outputDir.replace("{folderName}", dir.toString());
+ logger.info("outputDir 1={}", outputDir);
+ outputDir = outputDir.replace("\\watchedFolders", "");
+ outputDir = outputDir.replace("//watchedFolders", "");
+ outputDir = outputDir.replace("\\\\watchedFolders", "");
+ outputDir = outputDir.replace("/watchedFolders", "");
+
+ Path outputPath;
+ logger.info("outputDir 2={}", outputDir);
+ if (Paths.get(outputDir).isAbsolute()) {
+ // If it's an absolute path, use it directly
+ outputPath = Paths.get(outputDir);
+ } else {
+ // If it's a relative path, make it relative to the current working directory
+ outputPath = Paths.get(".", outputDir);
+ }
+
+ logger.info("outputPath={}", outputPath);
+
+ if (!Files.exists(outputPath)) {
+ try {
+ Files.createDirectories(outputPath);
+ logger.info("Created directory: {}", outputPath);
+ } catch (IOException e) {
+ logger.error("Error creating directory: {}", outputPath, e);
+ return;
+ }
+ }
+ logger.info("outputPath {}", outputPath);
+ logger.info("outputPath.resolve(outputFileName).toString() {}", outputPath.resolve(outputFileName).toString());
+ File newFile = new File(outputPath.resolve(outputFileName).toString());
+ OutputStream os = new FileOutputStream(newFile);
+ os.write(((ByteArrayResource)resource).getByteArray());
+ os.close();
+ logger.info("made {}", outputPath.resolve(outputFileName));
+ }
+
+ // If successful, delete the original files
+ for (File file : filesToProcess) {
+ Files.deleteIfExists(processingDir.resolve(file.getName()));
+ }
+ } catch (Exception e) {
+ // If an error occurs, move the original files back
+ for (File file : filesToProcess) {
+ Files.move(processingDir.resolve(file.getName()), file.toPath());
+ }
+ throw e;
+ }
+ }
+ }
+ }
+
+ List processFiles(List outputFiles, String jsonString) throws Exception {
+
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode jsonNode = mapper.readTree(jsonString);
+
+ JsonNode pipelineNode = jsonNode.get("pipeline");
+ logger.info("Running pipelineNode: {}", pipelineNode);
+ ByteArrayOutputStream logStream = new ByteArrayOutputStream();
+ PrintStream logPrintStream = new PrintStream(logStream);
+
+ boolean hasErrors = false;
+
+ for (JsonNode operationNode : pipelineNode) {
+ String operation = operationNode.get("operation").asText();
+ logger.info("Running operation: {}", operation);
+ JsonNode parametersNode = operationNode.get("parameters");
+ String inputFileExtension = "";
+ if (operationNode.has("inputFileType")) {
+ inputFileExtension = operationNode.get("inputFileType").asText();
+ } else {
+ inputFileExtension = ".pdf";
+ }
+
+ List newOutputFiles = new ArrayList<>();
+ boolean hasInputFileType = false;
+
+ for (Resource file : outputFiles) {
+ if (file.getFilename().endsWith(inputFileExtension)) {
+ hasInputFileType = true;
+ MultiValueMap body = new LinkedMultiValueMap<>();
+ body.add("fileInput", file);
+
+ Iterator> parameters = parametersNode.fields();
+ while (parameters.hasNext()) {
+ Map.Entry parameter = parameters.next();
+ body.add(parameter.getKey(), parameter.getValue().asText());
+ }
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.MULTIPART_FORM_DATA);
+
+ HttpEntity> entity = new HttpEntity<>(body, headers);
+
+ RestTemplate restTemplate = new RestTemplate();
+ String url = "http://localhost:8080/" + operation;
+
+ ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
+
+ // If the operation is filter and the response body is null or empty, skip this file
+ if (operation.startsWith("filter-") && (response.getBody() == null || response.getBody().length == 0)) {
+ logger.info("Skipping file due to failing {}", operation);
+ continue;
+ }
+
+ if (!response.getStatusCode().equals(HttpStatus.OK)) {
+ logPrintStream.println("Error: " + response.getBody());
+ hasErrors = true;
+ continue;
+ }
+
+
+ // Define filename
+ String filename;
+ if ("auto-rename".equals(operation)) {
+ // If the operation is "auto-rename", generate a new filename.
+ // This is a simple example of generating a filename using current timestamp.
+ // Modify as per your needs.
+ filename = "file_" + System.currentTimeMillis();
+ } else {
+ // Otherwise, keep the original filename.
+ filename = file.getFilename();
+ }
+
+ // Check if the response body is a zip file
+ if (isZip(response.getBody())) {
+ // Unzip the file and add all the files to the new output files
+ newOutputFiles.addAll(unzip(response.getBody()));
+ } else {
+ Resource outputResource = new ByteArrayResource(response.getBody()) {
+ @Override
+ public String getFilename() {
+ return filename;
+ }
+ };
+ newOutputFiles.add(outputResource);
+ }
+ }
+
+ if (!hasInputFileType) {
+ logPrintStream.println(
+ "No files with extension " + inputFileExtension + " found for operation " + operation);
+ hasErrors = true;
+ }
+
+ outputFiles = newOutputFiles;
+ }
+ logPrintStream.close();
+
+ }
+ if (hasErrors) {
+ logger.error("Errors occurred during processing. Log: {}", logStream.toString());
+ }
+ return outputFiles;
+ }
+
+ List handleFiles(File[] files, String jsonString) throws Exception {
+ if(files == null || files.length == 0) {
+ logger.info("No files");
+ return null;
+ }
+
+ logger.info("Handling files: {} files, with JSON string of length: {}", files.length, jsonString.length());
+
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode jsonNode = mapper.readTree(jsonString);
+
+ JsonNode pipelineNode = jsonNode.get("pipeline");
+
+ boolean hasErrors = false;
+ List outputFiles = new ArrayList<>();
+
+ for (File file : files) {
+ Path path = Paths.get(file.getAbsolutePath());
+ System.out.println("Reading file: " + path); // debug statement
+
+ if (Files.exists(path)) {
+ Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) {
+ @Override
+ public String getFilename() {
+ return file.getName();
+ }
+ };
+ outputFiles.add(fileResource);
+ } else {
+ System.out.println("File not found: " + path); // debug statement
+ }
+ }
+ logger.info("Files successfully loaded. Starting processing...");
+ return processFiles(outputFiles, jsonString);
+ }
+
+ List handleFiles(MultipartFile[] files, String jsonString) throws Exception {
+ if(files == null || files.length == 0) {
+ logger.info("No files");
+ return null;
+ }
+ logger.info("Handling files: {} files, with JSON string of length: {}", files.length, jsonString.length());
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode jsonNode = mapper.readTree(jsonString);
+
+ JsonNode pipelineNode = jsonNode.get("pipeline");
+
+ boolean hasErrors = false;
+ List outputFiles = new ArrayList<>();
+
+ for (MultipartFile file : files) {
+ Resource fileResource = new ByteArrayResource(file.getBytes()) {
+ @Override
+ public String getFilename() {
+ return file.getOriginalFilename();
+ }
+ };
+ outputFiles.add(fileResource);
+ }
+ logger.info("Files successfully loaded. Starting processing...");
+ return processFiles(outputFiles, jsonString);
+ }
+
+ @PostMapping("/handleData")
+ public ResponseEntity handleData(@RequestPart("fileInput") MultipartFile[] files,
+ @RequestParam("json") String jsonString) {
+ logger.info("Received POST request to /handleData with {} files", files.length);
+ try {
+ List outputFiles = handleFiles(files, jsonString);
+
+ if (outputFiles != null && outputFiles.size() == 1) {
+ // If there is only one file, return it directly
+ Resource singleFile = outputFiles.get(0);
+ InputStream is = singleFile.getInputStream();
+ byte[] bytes = new byte[(int) singleFile.contentLength()];
+ is.read(bytes);
+ is.close();
+
+ logger.info("Returning single file response...");
+ return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(),
+ MediaType.APPLICATION_OCTET_STREAM);
+ } else if (outputFiles == null) {
+ return null;
+ }
+
+ // Create a ByteArrayOutputStream to hold the zip
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ZipOutputStream zipOut = new ZipOutputStream(baos);
+
+ // Loop through each file and add it to the zip
+ for (Resource file : outputFiles) {
+ ZipEntry zipEntry = new ZipEntry(file.getFilename());
+ zipOut.putNextEntry(zipEntry);
+
+ // Read the file into a byte array
+ InputStream is = file.getInputStream();
+ byte[] bytes = new byte[(int) file.contentLength()];
+ is.read(bytes);
+
+ // Write the bytes of the file to the zip
+ zipOut.write(bytes, 0, bytes.length);
+ zipOut.closeEntry();
+
+ is.close();
+ }
+
+ zipOut.close();
+
+ logger.info("Returning zipped file response...");
+ return WebResponseUtils.boasToWebResponse(baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
+ } catch (Exception e) {
+ logger.error("Error handling data: ", e);
+ return null;
+ }
+ }
+
+ private boolean isZip(byte[] data) {
+ if (data == null || data.length < 4) {
+ return false;
+ }
+
+ // Check the first four bytes of the data against the standard zip magic number
+ return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04;
+ }
+
+ private List unzip(byte[] data) throws IOException {
+ logger.info("Unzipping data of length: {}", data.length);
+ List unzippedFiles = new ArrayList<>();
+
+ try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
+ ZipInputStream zis = new ZipInputStream(bais)) {
+
+ ZipEntry entry;
+ while ((entry = zis.getNextEntry()) != null) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+ int count;
+
+ while ((count = zis.read(buffer)) != -1) {
+ baos.write(buffer, 0, count);
+ }
+
+ final String filename = entry.getName();
+ Resource fileResource = new ByteArrayResource(baos.toByteArray()) {
+ @Override
+ public String getFilename() {
+ return filename;
+ }
+ };
+
+ // If the unzipped file is a zip file, unzip it
+ if (isZip(baos.toByteArray())) {
+ logger.info("File {} is a zip file. Unzipping...", filename);
+ unzippedFiles.addAll(unzip(baos.toByteArray()));
+ } else {
+ unzippedFiles.add(fileResource);
+ }
+ }
+ }
+
+ logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
+ return unzippedFiles;
+ }
+
+}
diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java b/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java
new file mode 100644
index 00000000..ccc19b09
--- /dev/null
+++ b/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java
@@ -0,0 +1,140 @@
+package stirling.software.SPDF.controller.api.security;
+import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.apache.pdfbox.pdmodel.PDResources;
+import org.apache.pdfbox.pdmodel.PDPageTree;
+import org.apache.pdfbox.pdmodel.common.PDMetadata;
+import org.apache.pdfbox.pdmodel.common.PDStream;
+import org.apache.pdfbox.pdmodel.interactive.action.*;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
+import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
+import org.apache.pdfbox.pdmodel.interactive.form.PDField;
+import org.apache.pdfbox.pdmodel.interactive.form.PDNonTerminalField;
+import org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Schema;
+import stirling.software.SPDF.utils.WebResponseUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+@RestController
+public class SanitizeController {
+
+ @PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf")
+ @Operation(summary = "Sanitize a PDF file",
+ description = "This endpoint processes a PDF file and removes specific elements based on the provided options. Input:PDF Output:PDF Type:SISO")
+ public ResponseEntity sanitizePDF(
+ @RequestPart(required = true, value = "fileInput")
+ @Parameter(description = "The input PDF file to be sanitized")
+ MultipartFile inputFile,
+ @RequestParam(name = "removeJavaScript", required = false, defaultValue = "true")
+ @Parameter(description = "Remove JavaScript actions from the PDF if set to true")
+ Boolean removeJavaScript,
+ @RequestParam(name = "removeEmbeddedFiles", required = false, defaultValue = "true")
+ @Parameter(description = "Remove embedded files from the PDF if set to true")
+ Boolean removeEmbeddedFiles,
+ @RequestParam(name = "removeMetadata", required = false, defaultValue = "true")
+ @Parameter(description = "Remove metadata from the PDF if set to true")
+ Boolean removeMetadata,
+ @RequestParam(name = "removeLinks", required = false, defaultValue = "true")
+ @Parameter(description = "Remove links from the PDF if set to true")
+ Boolean removeLinks,
+ @RequestParam(name = "removeFonts", required = false, defaultValue = "true")
+ @Parameter(description = "Remove fonts from the PDF if set to true")
+ Boolean removeFonts) throws IOException {
+
+ try (PDDocument document = PDDocument.load(inputFile.getInputStream())) {
+ if (removeJavaScript) {
+ sanitizeJavaScript(document);
+ }
+
+ if (removeEmbeddedFiles) {
+ sanitizeEmbeddedFiles(document);
+ }
+
+ if (removeMetadata) {
+ sanitizeMetadata(document);
+ }
+
+ if (removeLinks) {
+ sanitizeLinks(document);
+ }
+
+ if (removeFonts) {
+ sanitizeFonts(document);
+ }
+
+ return WebResponseUtils.pdfDocToWebResponse(document, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_sanitized.pdf");
+ }
+ }
+ private void sanitizeJavaScript(PDDocument document) throws IOException {
+ for (PDPage page : document.getPages()) {
+ for (PDAnnotation annotation : page.getAnnotations()) {
+ if (annotation instanceof PDAnnotationWidget) {
+ PDAnnotationWidget widget = (PDAnnotationWidget) annotation;
+ PDAction action = widget.getAction();
+ if (action instanceof PDActionJavaScript) {
+ widget.setAction(null);
+ }
+ }
+ }
+ PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
+ if (acroForm != null) {
+ for (PDField field : acroForm.getFields()) {
+ if (field.getActions().getF() instanceof PDActionJavaScript) {
+ field.getActions().setF(null);
+ }
+ }
+ }
+ }
+ }
+
+ private void sanitizeEmbeddedFiles(PDDocument document) {
+ PDPageTree allPages = document.getPages();
+
+ for (PDPage page : allPages) {
+ PDResources res = page.getResources();
+
+ // Remove embedded files from the PDF
+ res.getCOSObject().removeItem(COSName.getPDFName("EmbeddedFiles"));
+ }
+ }
+
+
+ private void sanitizeMetadata(PDDocument document) {
+ PDMetadata metadata = document.getDocumentCatalog().getMetadata();
+ if (metadata != null) {
+ document.getDocumentCatalog().setMetadata(null);
+ }
+ }
+
+
+
+ private void sanitizeLinks(PDDocument document) throws IOException {
+ for (PDPage page : document.getPages()) {
+ for (PDAnnotation annotation : page.getAnnotations()) {
+ if (annotation instanceof PDAnnotationLink) {
+ PDAction action = ((PDAnnotationLink) annotation).getAction();
+ if (action instanceof PDActionLaunch || action instanceof PDActionURI) {
+ ((PDAnnotationLink) annotation).setAction(null);
+ }
+ }
+ }
+ }
+ }
+
+ private void sanitizeFonts(PDDocument document) {
+ for (PDPage page : document.getPages()) {
+ page.getResources().getCOSObject().removeItem(COSName.getPDFName("Font"));
+ }
+ }
+
+}
diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java b/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java
index 4ef8604b..a8271207 100644
--- a/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java
+++ b/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java
@@ -1,12 +1,15 @@
package stirling.software.SPDF.controller.api.security;
import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.util.Arrays;
-import java.util.List;
+
+import javax.imageio.ImageIO;
import org.apache.commons.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
@@ -15,6 +18,8 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
+import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
+import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
import org.apache.pdfbox.util.Matrix;
import org.springframework.core.io.ClassPathResource;
@@ -30,124 +35,164 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils;
import io.swagger.v3.oas.annotations.media.Schema;
+
@RestController
@Tag(name = "Security", description = "Security APIs")
public class WatermarkController {
- @PostMapping(consumes = "multipart/form-data", value = "/add-watermark")
- @Operation(summary = "Add watermark to a PDF file",
- description = "This endpoint adds a watermark to a given PDF file. Users can specify the watermark text, font size, rotation, opacity, width spacer, and height spacer. Input:PDF Output:PDF Type:SISO")
- public ResponseEntity addWatermark(
- @RequestPart(required = true, value = "fileInput")
- @Parameter(description = "The input PDF file to add a watermark")
- MultipartFile pdfFile,
- @RequestParam(defaultValue = "roman", name = "alphabet")
- @Parameter(description = "The selected alphabet",
- schema = @Schema(type = "string",
- allowableValues = {"roman","arabic","japanese","korean","chinese"},
- defaultValue = "roman"))
- String alphabet,
- @RequestParam("watermarkText")
- @Parameter(description = "The watermark text to add to the PDF file")
- String watermarkText,
- @RequestParam(defaultValue = "30", name = "fontSize")
- @Parameter(description = "The font size of the watermark text", example = "30")
- float fontSize,
- @RequestParam(defaultValue = "0", name = "rotation")
- @Parameter(description = "The rotation of the watermark text in degrees", example = "0")
- float rotation,
- @RequestParam(defaultValue = "0.5", name = "opacity")
- @Parameter(description = "The opacity of the watermark text (0.0 - 1.0)", example = "0.5")
- float opacity,
- @RequestParam(defaultValue = "50", name = "widthSpacer")
- @Parameter(description = "The width spacer between watermark texts", example = "50")
- int widthSpacer,
- @RequestParam(defaultValue = "50", name = "heightSpacer")
- @Parameter(description = "The height spacer between watermark texts", example = "50")
- int heightSpacer) throws IOException, Exception {
+ @PostMapping(consumes = "multipart/form-data", value = "/add-watermark")
+ @Operation(summary = "Add watermark to a PDF file", description = "This endpoint adds a watermark to a given PDF file. Users can specify the watermark type (text or image), rotation, opacity, width spacer, and height spacer. Input:PDF Output:PDF Type:SISO")
+ public ResponseEntity addWatermark(
+ @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to add a watermark") MultipartFile pdfFile,
+ @RequestPart(required = true) @Parameter(description = "The watermark type (text or image)") String watermarkType,
+ @RequestPart(required = false) @Parameter(description = "The watermark text") String watermarkText,
+ @RequestPart(required = false) @Parameter(description = "The watermark image") MultipartFile watermarkImage,
+
+ @RequestParam(defaultValue = "roman", name = "alphabet") @Parameter(description = "The selected alphabet",
+ schema = @Schema(type = "string",
+ allowableValues = {"roman","arabic","japanese","korean","chinese"},
+ defaultValue = "roman")) String alphabet,
+ @RequestParam(defaultValue = "30", name = "fontSize") @Parameter(description = "The font size of the watermark text", example = "30") float fontSize,
+ @RequestParam(defaultValue = "0", name = "rotation") @Parameter(description = "The rotation of the watermark in degrees", example = "0") float rotation,
+ @RequestParam(defaultValue = "0.5", name = "opacity") @Parameter(description = "The opacity of the watermark (0.0 - 1.0)", example = "0.5") float opacity,
+ @RequestParam(defaultValue = "50", name = "widthSpacer") @Parameter(description = "The width spacer between watermark elements", example = "50") int widthSpacer,
+ @RequestParam(defaultValue = "50", name = "heightSpacer") @Parameter(description = "The height spacer between watermark elements", example = "50") int heightSpacer)
+ throws IOException, Exception {
- // Load the input PDF
- PDDocument document = PDDocument.load(pdfFile.getInputStream());
- String producer = document.getDocumentInformation().getProducer();
- // Create a page in the document
- for (PDPage page : document.getPages()) {
+ // Load the input PDF
+ PDDocument document = PDDocument.load(pdfFile.getInputStream());
- // Get the page's content stream
- PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true);
+ // Create a page in the document
+ for (PDPage page : document.getPages()) {
- // Set transparency
- PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
- graphicsState.setNonStrokingAlphaConstant(opacity);
- contentStream.setGraphicsStateParameters(graphicsState);
+ // Get the page's content stream
+ PDPageContentStream contentStream = new PDPageContentStream(document, page,
+ PDPageContentStream.AppendMode.APPEND, true);
+ // Set transparency
+ PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
+ graphicsState.setNonStrokingAlphaConstant(opacity);
+ contentStream.setGraphicsStateParameters(graphicsState);
- String resourceDir = "";
- PDFont font = PDType1Font.HELVETICA_BOLD;
- switch (alphabet) {
- case "arabic":
- resourceDir = "static/fonts/NotoSansArabic-Regular.ttf";
- break;
- case "japanese":
- resourceDir = "static/fonts/Meiryo.ttf";
- break;
- case "korean":
- resourceDir = "static/fonts/malgun.ttf";
- break;
- case "chinese":
- resourceDir = "static/fonts/SimSun.ttf";
- break;
- case "roman":
- default:
- resourceDir = "static/fonts/NotoSans-Regular.ttf";
- break;
- }
+ if (watermarkType.equalsIgnoreCase("text")) {
+ addTextWatermark(contentStream, watermarkText, document, page, rotation, widthSpacer, heightSpacer,
+ fontSize, alphabet);
+ } else if (watermarkType.equalsIgnoreCase("image")) {
+ addImageWatermark(contentStream, watermarkImage, document, page, rotation, widthSpacer, heightSpacer,
+ fontSize);
+ }
+ // Close the content stream
+ contentStream.close();
+ }
+
+ return WebResponseUtils.pdfDocToWebResponse(document,
+ pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf");
+ }
+
+ private void addTextWatermark(PDPageContentStream contentStream, String watermarkText, PDDocument document,
+ PDPage page, float rotation, int widthSpacer, int heightSpacer, float fontSize, String alphabet) throws IOException {
+ String resourceDir = "";
+ PDFont font = PDType1Font.HELVETICA_BOLD;
+ switch (alphabet) {
+ case "arabic":
+ resourceDir = "static/fonts/NotoSansArabic-Regular.ttf";
+ break;
+ case "japanese":
+ resourceDir = "static/fonts/Meiryo.ttf";
+ break;
+ case "korean":
+ resourceDir = "static/fonts/malgun.ttf";
+ break;
+ case "chinese":
+ resourceDir = "static/fonts/SimSun.ttf";
+ break;
+ case "roman":
+ default:
+ resourceDir = "static/fonts/NotoSans-Regular.ttf";
+ break;
+ }
+
+
+ if(!resourceDir.equals("")) {
+ ClassPathResource classPathResource = new ClassPathResource(resourceDir);
+ String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
+ File tempFile = File.createTempFile("NotoSansFont", fileExtension);
+ try (InputStream is = classPathResource.getInputStream(); FileOutputStream os = new FileOutputStream(tempFile)) {
+ IOUtils.copy(is, os);
+ }
- if(!resourceDir.equals("")) {
- ClassPathResource classPathResource = new ClassPathResource(resourceDir);
- String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
- File tempFile = File.createTempFile("NotoSansFont", fileExtension);
- try (InputStream is = classPathResource.getInputStream(); FileOutputStream os = new FileOutputStream(tempFile)) {
- IOUtils.copy(is, os);
- }
-
- font = PDType0Font.load(document, tempFile);
- tempFile.deleteOnExit();
- }
- contentStream.beginText();
- contentStream.setFont(font, fontSize);
- contentStream.setNonStrokingColor(Color.LIGHT_GRAY);
-
- // Set size and location of watermark
- float pageWidth = page.getMediaBox().getWidth();
- float pageHeight = page.getMediaBox().getHeight();
- float watermarkWidth = widthSpacer + font.getStringWidth(watermarkText) * fontSize / 1000;
- float watermarkHeight = heightSpacer + fontSize;
- int watermarkRows = (int) (pageHeight / watermarkHeight + 1);
- int watermarkCols = (int) (pageWidth / watermarkWidth + 1);
-
- // Add the watermark text
- for (int i = 0; i < watermarkRows; i++) {
- for (int j = 0; j < watermarkCols; j++) {
-
- if(producer.contains("Google Docs")) {
- //This fixes weird unknown google docs y axis rotation/flip issue
- //TODO: Long term fix one day
- //contentStream.setTextMatrix(1, 0, 0, -1, j * watermarkWidth, pageHeight - i * watermarkHeight);
- Matrix matrix = new Matrix(1, 0, 0, -1, j * watermarkWidth, pageHeight - i * watermarkHeight);
- contentStream.setTextMatrix(matrix);
- } else {
- contentStream.setTextMatrix(Matrix.getRotateInstance((float) Math.toRadians(rotation), j * watermarkWidth, i * watermarkHeight));
- }
- contentStream.showTextWithPositioning(new Object[] { watermarkText });
- }
- }
- contentStream.endText();
-
- // Close the content stream
- contentStream.close();
+ font = PDType0Font.load(document, tempFile);
+ tempFile.deleteOnExit();
}
- return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf");
- }
+
+ contentStream.setFont(font, fontSize);
+ contentStream.setNonStrokingColor(Color.LIGHT_GRAY);
+
+ // Set size and location of text watermark
+ float watermarkWidth = widthSpacer + font.getStringWidth(watermarkText) * fontSize / 1000;
+ float watermarkHeight = heightSpacer + fontSize;
+ float pageWidth = page.getMediaBox().getWidth();
+ float pageHeight = page.getMediaBox().getHeight();
+ int watermarkRows = (int) (pageHeight / watermarkHeight + 1);
+ int watermarkCols = (int) (pageWidth / watermarkWidth + 1);
+
+ // Add the text watermark
+ for (int i = 0; i < watermarkRows; i++) {
+ for (int j = 0; j < watermarkCols; j++) {
+ contentStream.beginText();
+ contentStream.setTextMatrix(Matrix.getRotateInstance((float) Math.toRadians(rotation),
+ j * watermarkWidth, i * watermarkHeight));
+ contentStream.showText(watermarkText);
+ contentStream.endText();
+ }
+ }
+ }
+
+ private void addImageWatermark(PDPageContentStream contentStream, MultipartFile watermarkImage, PDDocument document, PDPage page, float rotation,
+ int widthSpacer, int heightSpacer, float fontSize) throws IOException {
+
+// Load the watermark image
+BufferedImage image = ImageIO.read(watermarkImage.getInputStream());
+
+// Compute width based on original aspect ratio
+float aspectRatio = (float) image.getWidth() / (float) image.getHeight();
+
+// Desired physical height (in PDF points)
+float desiredPhysicalHeight = fontSize ;
+
+// Desired physical width based on the aspect ratio
+float desiredPhysicalWidth = desiredPhysicalHeight * aspectRatio;
+
+// Convert the BufferedImage to PDImageXObject
+PDImageXObject xobject = LosslessFactory.createFromImage(document, image);
+
+// Calculate the number of rows and columns for watermarks
+float pageWidth = page.getMediaBox().getWidth();
+float pageHeight = page.getMediaBox().getHeight();
+int watermarkRows = (int) ((pageHeight + heightSpacer) / (desiredPhysicalHeight + heightSpacer));
+int watermarkCols = (int) ((pageWidth + widthSpacer) / (desiredPhysicalWidth + widthSpacer));
+
+for (int i = 0; i < watermarkRows; i++) {
+for (int j = 0; j < watermarkCols; j++) {
+float x = j * (desiredPhysicalWidth + widthSpacer);
+float y = i * (desiredPhysicalHeight + heightSpacer);
+
+// Save the graphics state
+contentStream.saveGraphicsState();
+
+// Create rotation matrix and rotate
+contentStream.transform(Matrix.getTranslateInstance(x + desiredPhysicalWidth / 2, y + desiredPhysicalHeight / 2));
+contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0));
+contentStream.transform(Matrix.getTranslateInstance(-desiredPhysicalWidth / 2, -desiredPhysicalHeight / 2));
+
+// Draw the image and restore the graphics state
+contentStream.drawImage(xobject, 0, 0, desiredPhysicalWidth, desiredPhysicalHeight);
+contentStream.restoreGraphicsState();
+}
+
+}
+
+ }
}
diff --git a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java
index 11a7c9bf..dcf953a5 100644
--- a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java
+++ b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java
@@ -1,21 +1,76 @@
package stirling.software.SPDF.controller.web;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.HashMap;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ModelAttribute;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
-
@Controller
@Tag(name = "General", description = "General APIs")
public class GeneralWebController {
- @GetMapping("/pipeline")
- @Hidden
- public String pipelineForm(Model model) {
- model.addAttribute("currentPage", "pipeline");
- return "pipeline";
+
+ @GetMapping("/pipeline")
+ @Hidden
+ public String pipelineForm(Model model) {
+ model.addAttribute("currentPage", "pipeline");
+
+ List pipelineConfigs = new ArrayList<>();
+ try (Stream paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) {
+ List jsonFiles = paths
+ .filter(Files::isRegularFile)
+ .filter(p -> p.toString().endsWith(".json"))
+ .collect(Collectors.toList());
+
+ for (Path jsonFile : jsonFiles) {
+ String content = Files.readString(jsonFile, StandardCharsets.UTF_8);
+ pipelineConfigs.add(content);
+ }
+ List
+
+
+
+