From e3c8af7e54d00537c31feeeb49f048a9d64d423b Mon Sep 17 00:00:00 2001 From: sbplat <71648843+sbplat@users.noreply.github.com> Date: Fri, 29 Dec 2023 21:34:36 -0500 Subject: [PATCH 01/47] chore: use spotless googleJavaFormat --- build.gradle | 42 +++++++++++++------ .../api/misc/FakeScanControllerWIP.java | 8 ++-- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/build.gradle b/build.gradle index 8002ee7a..95a0930d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,11 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.1.2' - id 'io.spring.dependency-management' version '1.1.3' - id 'org.springdoc.openapi-gradle-plugin' version '1.8.0' - id "io.swagger.swaggerhub" version "1.3.2" - id 'edu.sc.seis.launch4j' version '3.0.5' + id 'java' + id 'org.springframework.boot' version '3.1.2' + id 'io.spring.dependency-management' version '1.1.3' + id 'org.springdoc.openapi-gradle-plugin' version '1.8.0' + id "io.swagger.swaggerhub" version "1.3.2" + id 'edu.sc.seis.launch4j' version '3.0.5' + id 'com.diffplug.spotless' version '6.23.3' } group = 'stirling.software' @@ -12,7 +13,7 @@ version = '0.18.1' sourceCompatibility = '17' repositories { - mavenCentral() + mavenCentral() } sourceSets { @@ -61,12 +62,27 @@ launch4j { messagesInstanceAlreadyExists="Stirling-PDF is already running." } +spotless { + java { + target project.fileTree('src/main/java') + + googleJavaFormat('1.19.1').aosp().reorderImports(false) + + importOrder('java', 'javax', 'org', 'com', 'net', 'io') + toggleOffOn() + formatAnnotations() + trimTrailingWhitespace() + indentWithSpaces() + endWithNewline() + } +} + dependencies { - //security updates - implementation 'ch.qos.logback:logback-classic:1.4.14' - implementation 'ch.qos.logback:logback-core:1.4.14' - implementation 'org.springframework:spring-webmvc:6.0.15' - + //security updates + implementation 'ch.qos.logback:logback-classic:1.4.14' + implementation 'ch.qos.logback:logback-core:1.4.14' + implementation 'org.springframework:spring-webmvc:6.0.15' + implementation 'org.yaml:snakeyaml:2.1' implementation 'org.springframework.boot:spring-boot-starter-web:3.2.1' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.2.1' @@ -164,7 +180,7 @@ jar { } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } task printVersion { diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/FakeScanControllerWIP.java b/src/main/java/stirling/software/SPDF/controller/api/misc/FakeScanControllerWIP.java index 099e8411..9e9d8ba6 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/FakeScanControllerWIP.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/FakeScanControllerWIP.java @@ -3,21 +3,21 @@ package stirling.software.SPDF.controller.api.misc; import java.awt.Color; import java.awt.geom.AffineTransform; import java.awt.image.AffineTransformOp; -//Required for image manipulation + import java.awt.image.BufferedImage; import java.awt.image.BufferedImageOp; import java.awt.image.ConvolveOp; import java.awt.image.Kernel; import java.awt.image.RescaleOp; import java.io.ByteArrayOutputStream; -//Required for file input/output + import java.io.File; import java.io.IOException; import java.security.SecureRandom; -//Other required classes + import java.util.Random; -//Required for image input/output + import javax.imageio.ImageIO; import org.apache.pdfbox.pdmodel.PDDocument; From 7b43fca6fc7f61d511924a0563bad649b57f8082 Mon Sep 17 00:00:00 2001 From: sbplat <71648843+sbplat@users.noreply.github.com> Date: Fri, 29 Dec 2023 21:59:36 -0500 Subject: [PATCH 02/47] ci: gradle build instead of assemble --- .github/workflows/build.yml | 34 ++++++++++++++++++++++ .github/workflows/codeql.yml | 55 ------------------------------------ 2 files changed, 34 insertions(+), 55 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..74d1f8f5 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,34 @@ +name: "Build repo" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - uses: gradle/gradle-build-action@v2.4.2 + with: + gradle-version: 7.6 + arguments: build --no-build-cache diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 55500892..00000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,55 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of - -name: "Build repo" - -on: - push: - branches: [ "main" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] - schedule: - - cron: '15 12 * * 1' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - - # - name: Initialize CodeQL - # uses: github/codeql-action/init@v2 - # with: - # languages: java - - - uses: gradle/gradle-build-action@v2.4.2 - with: - gradle-version: 7.6 - arguments: assemble --no-build-cache - - #- name: Perform CodeQL analysis - # uses: github/codeql-action/analyze@v2 From 4612b051994ab87bdb12a897800f3c8032b8d985 Mon Sep 17 00:00:00 2001 From: mannam <101550345+ManoharMannam@users.noreply.github.com> Date: Sat, 30 Dec 2023 13:32:54 +0530 Subject: [PATCH 03/47] overriding previously selected files issue#529 --- src/main/resources/static/js/merge.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/static/js/merge.js b/src/main/resources/static/js/merge.js index d27730b9..7b5de12c 100644 --- a/src/main/resources/static/js/merge.js +++ b/src/main/resources/static/js/merge.js @@ -10,7 +10,6 @@ document.getElementById("fileInput-input").addEventListener("change", function() function displayFiles(files) { var list = document.getElementById("selectedFiles"); - list.innerHTML = ""; for (var i = 0; i < files.length; i++) { var item = document.createElement("li"); From 56cbb4381bcc4cc9b51103fd8f45a7e9ac103032 Mon Sep 17 00:00:00 2001 From: NeilJared Date: Sat, 30 Dec 2023 09:09:27 +0100 Subject: [PATCH 04/47] Update messages_es_ES.properties --- src/main/resources/messages_es_ES.properties | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/resources/messages_es_ES.properties b/src/main/resources/messages_es_ES.properties index d1ea8a40..4a520a50 100644 --- a/src/main/resources/messages_es_ES.properties +++ b/src/main/resources/messages_es_ES.properties @@ -256,9 +256,9 @@ home.removeBlanks.title=Eliminar páginas en blanco home.removeBlanks.desc=Detectar y eliminar páginas en blanco de un documento removeBlanks.tags=limpieza,dinámica,sin contenido,organizar -home.removeAnnotations.title=Remove Annotations -home.removeAnnotations.desc=Removes all comments/annotations from a PDF -removeAnnotations.tags=comments,highlight,notes,markup,remove +home.removeAnnotations.title=Eliminar Anotaciones +home.removeAnnotations.desc=Eliminar todos los comentarios/anotaciones de un PDF +removeAnnotations.tags=comentarios,subrayar,notas,margen,eliminar home.compare.title=Comparar home.compare.desc=Comparar y mostrar las diferencias entre 2 documentos PDF @@ -355,7 +355,7 @@ home.overlay-pdfs.title=Superponer PDFs home.overlay-pdfs.desc=Superponer PDFs encima de otro PDF overlay-pdfs.tags=Superponer -home.split-by-sections.title=Dividir PDF por Seccioned +home.split-by-sections.title=Dividir PDF por Secciones home.split-by-sections.desc=Dividir cada página de un PDF en secciones verticales y horizontales más pequeñas split-by-sections.tags=Dividir sección, Dividir, Personalizar @@ -545,9 +545,9 @@ removeBlanks.submit=Eliminar espacios en blanco #removeAnnotations -removeAnnotations.title=Remove Annotations -removeAnnotations.header=Remove Annotations -removeAnnotations.submit=Remove +removeAnnotations.title=Eliminar anotaciones +removeAnnotations.header=Eliminar anotaciones +removeAnnotations.submit=Eliminar #compare @@ -872,7 +872,7 @@ split-by-size-or-count.submit=Enviar #overlay-pdfs overlay-pdfs.header=Superponer archivos PDF -overlay-pdfs.baseFile.label=Selleccione archivo PDF de base +overlay-pdfs.baseFile.label=Seleccione archivo PDF de base overlay-pdfs.overlayFiles.label=Seleccione archivos PDF a superponer overlay-pdfs.mode.label=Seleccione modo de superposición overlay-pdfs.mode.sequential=Superposición Sequencial From 6ca9001fe6115ded1d147051795d870ae1c9c9bf Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 30 Dec 2023 13:42:24 +0000 Subject: [PATCH 05/47] enable status check without apikey --- .../software/SPDF/config/security/SecurityConfiguration.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java index e0b439db..71394b09 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -90,7 +90,8 @@ public class SecurityConfiguration { return trimmedUri.startsWith("/login") || trimmedUri.endsWith(".svg") || trimmedUri.startsWith("/register") || trimmedUri.startsWith("/error") || trimmedUri.startsWith("/images/") || trimmedUri.startsWith("/public/") || - trimmedUri.startsWith("/css/") || trimmedUri.startsWith("/js/"); + trimmedUri.startsWith("/css/") || trimmedUri.startsWith("/js/") || + trimmedUri.startsWith("api/v1/info/status"); } ).permitAll() .anyRequest().authenticated() From a92479b505606c35699a54ffc26de472b18744fd Mon Sep 17 00:00:00 2001 From: albanobattistella <34811668+albanobattistella@users.noreply.github.com> Date: Sat, 30 Dec 2023 18:19:51 +0100 Subject: [PATCH 06/47] Update messages_it_IT.properties --- src/main/resources/messages_it_IT.properties | 36 ++++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/main/resources/messages_it_IT.properties b/src/main/resources/messages_it_IT.properties index 02cf6314..8801f014 100644 --- a/src/main/resources/messages_it_IT.properties +++ b/src/main/resources/messages_it_IT.properties @@ -119,7 +119,7 @@ adminUserSettings.role=Ruolo adminUserSettings.actions=Azioni adminUserSettings.apiUser=Utente API limitato adminUserSettings.webOnlyUser=Utente solo Web -adminUserSettings.demoUser=Demo User (No custom settings) +adminUserSettings.demoUser=Utente demo (nessuna impostazione personalizzata) adminUserSettings.forceChange=Forza l'utente a cambiare nome username/password all'accesso adminUserSettings.submit=Salva utente @@ -165,8 +165,8 @@ pdfOrganiser.tags=duplex,pari,dispari,ordinamento,spostamento home.addImage.title=Aggiungi Immagine -home.addImage.desc=Aggiungi un'immagine in un punto specifico del PDF (Work in progress) -addImage.tags=img,jpg,picture,photo +home.addImage.desc=Aggiungi un'immagine in un punto specifico del PDF (Lavori in corso) +addImage.tags=img,jpg,immagine,photo home.watermark.title=Aggiungi Filigrana home.watermark.desc=Aggiungi una filigrana al tuo PDF. @@ -242,7 +242,7 @@ ScannerImageSplit.tags=separa,rileva automaticamente,scansiona,multi-foto,organi home.sign.title=Firma home.sign.desc=Aggiungi una firma al PDF da disegno, testo o immagine. -sign.tags=autorizza,iniziali,firma-tracciata,segno-testo,firma-immagine +sign.tags=autorizza,iniziali,firma-tracciata,firma-testo,firma-immagine home.flatten.title=Appiattisci home.flatten.desc=Rimuovi tutti gli elementi interattivi e moduli da un PDF. @@ -256,9 +256,9 @@ home.removeBlanks.title=Rimuovi pagine vuote home.removeBlanks.desc=Trova e rimuovi pagine vuote da un PDF. removeBlanks.tags=pulire,semplificare,non contenere contenuti,organizzare -home.removeAnnotations.title=Remove Annotations -home.removeAnnotations.desc=Removes all comments/annotations from a PDF -removeAnnotations.tags=comments,highlight,notes,markup,remove +home.removeAnnotations.title=Rimuovi annotazioni +home.removeAnnotations.desc=Rimuove tutti i commenti/annotazioni da un PDF +removeAnnotations.tags=commenti,evidenziazioni,note,markup,rimozione home.compare.title=Compara home.compare.desc=Vedi e compara le differenze tra due PDF. @@ -443,7 +443,7 @@ sanitizePDF.selectText.1=Rimuovi le azioni JavaScript sanitizePDF.selectText.2=Rimuovi i file incorporati sanitizePDF.selectText.3=Rimuovi i metadati sanitizePDF.selectText.4=Rimuovi collegamenti -sanitizePDF.selectText.5=Rimuovi i fonts +sanitizePDF.selectText.5=Rimuovi i font sanitizePDF.submit=Pulisci PDF @@ -457,7 +457,7 @@ addPageNumbers.selectText.4=Numero di partenza addPageNumbers.selectText.5=Pagine da numerare addPageNumbers.selectText.6=Testo personalizzato addPageNumbers.customTextDesc=Testo personalizzato -addPageNumbers.numberPagesDesc=Quali pagine numerare, impostazione predefinita "all", accetta anche 1-5 o 2,5,9 ecc +addPageNumbers.numberPagesDesc=Quali pagine numerare, impostazione predefinita "tutte", accetta anche 1-5 o 2,5,9 ecc addPageNumbers.customNumberDesc=Il valore predefinito è {n}, accetta anche 'Pagina {n} di {total}', 'Testo-{n}', '{filename}-{n} addPageNumbers.submit=Aggiungi numeri di pagina @@ -545,9 +545,9 @@ removeBlanks.submit=Rimuovi #removeAnnotations -removeAnnotations.title=Remove Annotations -removeAnnotations.header=Remove Annotations -removeAnnotations.submit=Remove +removeAnnotations.title=Rimuovi Annotazioni +removeAnnotations.header=Remuovi Annotazioni +removeAnnotations.submit=Rimuovi #compare @@ -590,7 +590,7 @@ ScannerImageSplit.selectText.6=Imposta l'area minima di una foto (default: 10000 ScannerImageSplit.selectText.7=Area di contorno minima: ScannerImageSplit.selectText.8=Imposta l'area minima del contorno di una foto ScannerImageSplit.selectText.9=Spessore bordo: -ScannerImageSplit.selectText.10=Imposta lo spessore del bordo aggiunto o rimosso per prevenire bordi bianchi nel risultato (default: 1). +ScannerImageSplit.selectText.10=Imposta lo spessore del bordo aggiunto o rimosso per prevenire bordi bianchi nel risultato (predefinito: 1). #OCR @@ -794,7 +794,7 @@ changeMetadata.title=Titolo: changeMetadata.header=Cambia Proprietà changeMetadata.selectText.1=Imposta i dati che vuoi cambiare changeMetadata.selectText.2=Cancella tutte le proprietà -changeMetadata.selectText.3=Visualizza proprietà custom: +changeMetadata.selectText.3=Visualizza proprietà personalizzate: changeMetadata.author=Autore: changeMetadata.creationDate=Data di creazione (yyyy/MM/dd HH:mm:ss): changeMetadata.creator=Creatore: @@ -857,7 +857,7 @@ PDFToXML.submit=Converti PDFToCSV.title=Da PDF a CSV PDFToCSV.header=Da PDF a CSV PDFToCSV.prompt=Scegli la pagina per estrarre la tabella -PDFToCSV.submit=Estratto +PDFToCSV.submit=Estrai #split-by-size-or-count split-by-size-or-count.header=Dividi il PDF per dimensione o numero @@ -876,9 +876,9 @@ overlay-pdfs.baseFile.label=Seleziona File PDF di base overlay-pdfs.overlayFiles.label=Seleziona sovrapposizione file PDF overlay-pdfs.mode.label=Seleziona la modalità di sovrapposizione overlay-pdfs.mode.sequential=Sovrapposizione sequenziale -overlay-pdfs.mode.interleaved=Interleaved Overlay -overlay-pdfs.mode.fixedRepeat=Fixed Repeat Overlay -overlay-pdfs.counts.label=Overlay Counts (for Fixed Repeat Mode) +overlay-pdfs.mode.interleaved=Sovrapposizione interfogliata +overlay-pdfs.mode.fixedRepeat=Risolto il problema con la ripetizione della sovrapposizione +overlay-pdfs.counts.label=Numeri sovrapposti (per la modalità di ripetizione fissa) overlay-pdfs.counts.placeholder=Inserisci i numeri separati da virgole (ad esempio, 2,3,1) overlay-pdfs.position.label=Seleziona posizione di sovrapposizione overlay-pdfs.position.foreground=Primo piano From c853465d1d356c96471a0139a38dd7801be79935 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 30 Dec 2023 18:56:07 +0000 Subject: [PATCH 07/47] tests --- .../docker-compose-latest-lite-security.yml | 31 +++++ .../docker-compose-latest-lite.yml | 30 +++++ .../docker-compose-latest-security.yml | 31 +++++ ...ker-compose-latest-ultra-lite-security.yml | 31 +++++ .../docker-compose-latest-ultra-lite.yml | 30 +++++ exampleYmlFiles/docker-compose-latest.yml | 31 +++++ .../security/SecurityConfiguration.java | 2 +- .../security/UserAuthenticationFilter.java | 1 + .../software/SPDF/utils/RequestUriUtils.java | 3 +- test.sh | 122 ++++++++++++++++++ 10 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 exampleYmlFiles/docker-compose-latest-lite-security.yml create mode 100644 exampleYmlFiles/docker-compose-latest-lite.yml create mode 100644 exampleYmlFiles/docker-compose-latest-security.yml create mode 100644 exampleYmlFiles/docker-compose-latest-ultra-lite-security.yml create mode 100644 exampleYmlFiles/docker-compose-latest-ultra-lite.yml create mode 100644 exampleYmlFiles/docker-compose-latest.yml create mode 100644 test.sh diff --git a/exampleYmlFiles/docker-compose-latest-lite-security.yml b/exampleYmlFiles/docker-compose-latest-lite-security.yml new file mode 100644 index 00000000..98b95058 --- /dev/null +++ b/exampleYmlFiles/docker-compose-latest-lite-security.yml @@ -0,0 +1,31 @@ +version: '3.3' +services: + stirling-pdf: + container_name: Stirling-PDF-Lite-Security + image: frooodle/s-pdf:latest-lite-local + deploy: + resources: + limits: + memory: 1G + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"] + interval: 30s + timeout: 10s + retries: 3 + ports: + - 8080:8080 + volumes: + - /stirling/latest/data:/usr/share/tesseract-ocr/5/tessdata:rw + - /stirling/latest/config:/configs:rw + - /stirling/latest/logs:/logs:rw + environment: + DOCKER_ENABLE_SECURITY: "true" + SECURITY_ENABLELOGIN: "true" + SYSTEM_DEFAULTLOCALE: en_US + UI_APPNAME: Stirling-PDF + UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest + UI_APPNAMENAVBAR: Stirling-PDF Latest + SYSTEM_MAXFILESIZE: "100" + METRICS_ENABLED: "true" + SYSTEM_GOOGLEVISIBILITY: "true" + restart: on-failure:5 diff --git a/exampleYmlFiles/docker-compose-latest-lite.yml b/exampleYmlFiles/docker-compose-latest-lite.yml new file mode 100644 index 00000000..c2f18efe --- /dev/null +++ b/exampleYmlFiles/docker-compose-latest-lite.yml @@ -0,0 +1,30 @@ +version: '3.3' +services: + stirling-pdf: + container_name: Stirling-PDF-Lite + image: frooodle/s-pdf:latest-lite-local + deploy: + resources: + limits: + memory: 1G + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"] + interval: 20s + timeout: 10s + retries: 3 + ports: + - 8080:8080 + volumes: + - /stirling/latest/config:/configs:rw + - /stirling/latest/logs:/logs:rw + environment: + DOCKER_ENABLE_SECURITY: "false" + SECURITY_ENABLELOGIN: "false" + SYSTEM_DEFAULTLOCALE: en_US + UI_APPNAME: Stirling-PDF + UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest + UI_APPNAMENAVBAR: Stirling-PDF Latest + SYSTEM_MAXFILESIZE: "100" + METRICS_ENABLED: "true" + SYSTEM_GOOGLEVISIBILITY: "true" + restart: on-failure:5 diff --git a/exampleYmlFiles/docker-compose-latest-security.yml b/exampleYmlFiles/docker-compose-latest-security.yml new file mode 100644 index 00000000..b36a08a3 --- /dev/null +++ b/exampleYmlFiles/docker-compose-latest-security.yml @@ -0,0 +1,31 @@ +version: '3.3' +services: + stirling-pdf: + container_name: Stirling-PDF-Security + image: frooodle/s-pdf:latest-local + deploy: + resources: + limits: + memory: 1G + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"] + interval: 20s + timeout: 10s + retries: 3 + ports: + - 8080:8080 + volumes: + - /stirling/latest/data:/usr/share/tesseract-ocr/5/tessdata:rw + - /stirling/latest/config:/configs:rw + - /stirling/latest/logs:/logs:rw + environment: + DOCKER_ENABLE_SECURITY: "true" + SECURITY_ENABLELOGIN: "true" + SYSTEM_DEFAULTLOCALE: en_US + UI_APPNAME: Stirling-PDF + UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest + UI_APPNAMENAVBAR: Stirling-PDF Latest + SYSTEM_MAXFILESIZE: "100" + METRICS_ENABLED: "true" + SYSTEM_GOOGLEVISIBILITY: "true" + restart: on-failure:5 diff --git a/exampleYmlFiles/docker-compose-latest-ultra-lite-security.yml b/exampleYmlFiles/docker-compose-latest-ultra-lite-security.yml new file mode 100644 index 00000000..7b0d6c43 --- /dev/null +++ b/exampleYmlFiles/docker-compose-latest-ultra-lite-security.yml @@ -0,0 +1,31 @@ +version: '3.3' +services: + stirling-pdf: + container_name: Stirling-PDF-Ultra-Lite-Security + image: frooodle/s-pdf:latest-ultra-lite-local + deploy: + resources: + limits: + memory: 1G + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"] + interval: 30s + timeout: 10s + retries: 3 + ports: + - 8080:8080 + volumes: + - /stirling/latest/data:/usr/share/tesseract-ocr/5/tessdata:rw + - /stirling/latest/config:/configs:rw + - /stirling/latest/logs:/logs:rw + environment: + DOCKER_ENABLE_SECURITY: "true" + SECURITY_ENABLELOGIN: "true" + SYSTEM_DEFAULTLOCALE: en_US + UI_APPNAME: Stirling-PDF + UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest + UI_APPNAMENAVBAR: Stirling-PDF Latest + SYSTEM_MAXFILESIZE: "100" + METRICS_ENABLED: "true" + SYSTEM_GOOGLEVISIBILITY: "true" + restart: on-failure:5 diff --git a/exampleYmlFiles/docker-compose-latest-ultra-lite.yml b/exampleYmlFiles/docker-compose-latest-ultra-lite.yml new file mode 100644 index 00000000..5848873d --- /dev/null +++ b/exampleYmlFiles/docker-compose-latest-ultra-lite.yml @@ -0,0 +1,30 @@ +version: '3.3' +services: + stirling-pdf: + container_name: Stirling-PDF-Ultra-Lite + image: frooodle/s-pdf:latest-ultra-lite-local + deploy: + resources: + limits: + memory: 1G + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"] + interval: 20s + timeout: 10s + retries: 3 + ports: + - 8080:8080 + volumes: + - /stirling/latest/config:/configs:rw + - /stirling/latest/logs:/logs:rw + environment: + DOCKER_ENABLE_SECURITY: "false" + SECURITY_ENABLELOGIN: "false" + SYSTEM_DEFAULTLOCALE: en_US + UI_APPNAME: Stirling-PDF + UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest + UI_APPNAMENAVBAR: Stirling-PDF Latest + SYSTEM_MAXFILESIZE: "100" + METRICS_ENABLED: "true" + SYSTEM_GOOGLEVISIBILITY: "true" + restart: on-failure:5 diff --git a/exampleYmlFiles/docker-compose-latest.yml b/exampleYmlFiles/docker-compose-latest.yml new file mode 100644 index 00000000..e319695f --- /dev/null +++ b/exampleYmlFiles/docker-compose-latest.yml @@ -0,0 +1,31 @@ +version: '3.3' +services: + stirling-pdf: + container_name: Stirling-PDF + image: frooodle/s-pdf:latest-local + deploy: + resources: + limits: + memory: 1G + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"] + interval: 30s + timeout: 10s + retries: 3 + ports: + - 8080:8080 + volumes: + - /stirling/latest/data:/usr/share/tesseract-ocr/5/tessdata:rw + - /stirling/latest/config:/configs:rw + - /stirling/latest/logs:/logs:rw + environment: + DOCKER_ENABLE_SECURITY: "false" + SECURITY_ENABLELOGIN: "false" + SYSTEM_DEFAULTLOCALE: en_US + UI_APPNAME: Stirling-PDF + UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest + UI_APPNAMENAVBAR: Stirling-PDF Latest + SYSTEM_MAXFILESIZE: "100" + METRICS_ENABLED: "true" + SYSTEM_GOOGLEVISIBILITY: "true" + restart: on-failure:5 diff --git a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java index 71394b09..18fad46b 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -91,7 +91,7 @@ public class SecurityConfiguration { trimmedUri.startsWith("/register") || trimmedUri.startsWith("/error") || trimmedUri.startsWith("/images/") || trimmedUri.startsWith("/public/") || trimmedUri.startsWith("/css/") || trimmedUri.startsWith("/js/") || - trimmedUri.startsWith("api/v1/info/status"); + trimmedUri.startsWith("/api/v1/info/status"); } ).permitAll() .anyRequest().authenticated() diff --git a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java index ce77e5a4..2b98a9e5 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java @@ -102,6 +102,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { contextPath + "/css/", contextPath + "/js/", contextPath + "/pdfjs/", + contextPath + "/api/v1/info/status", contextPath + "/site.webmanifest" }; diff --git a/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java b/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java index 0046ee9f..6f07b573 100644 --- a/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java @@ -9,7 +9,8 @@ public class RequestUriUtils { || requestURI.startsWith("/images/") || requestURI.startsWith("/public/") || requestURI.startsWith("/pdfjs/") - || requestURI.endsWith(".svg"); + || requestURI.endsWith(".svg") + || requestURI.startsWith("/api/v1/info/status"); } diff --git a/test.sh b/test.sh new file mode 100644 index 00000000..3b84ef42 --- /dev/null +++ b/test.sh @@ -0,0 +1,122 @@ +#!/bin/bash + +# Function to check the health of the service with a timeout of 80 seconds +check_health() { + local service_name=$1 + local compose_file=$2 + local end=$((SECONDS+60)) + + echo "Waiting for $service_name to become healthy..." + until [ "$(docker inspect --format='{{json .State.Health.Status}}' "$service_name")" == '"healthy"' ] || [ $SECONDS -ge $end ]; do + sleep 10 + echo "Waiting..." + if [ $SECONDS -ge $end ]; then + echo "$service_name health check timed out after 80 seconds." + return 1 + fi + done + echo "$service_name is healthy!" + return 0 +} + +# Function to test a Docker Compose configuration +test_compose() { + local compose_file=$1 + local service_name=$2 + local status=0 + + echo "Testing $compose_file configuration..." + + # Start up the Docker Compose service + docker-compose -f "$compose_file" up -d + + # Wait for the service to become healthy + if check_health "$service_name" "$compose_file"; then + echo "$service_name test passed." + else + echo "$service_name test failed." + status=1 + fi + + # Perform additional tests if needed + + # Tear down the service + docker-compose -f "$compose_file" down + + return $status +} + +# Keep track of which tests passed and failed +declare -a passed_tests +declare -a failed_tests + +run_tests() { + local test_name=$1 + local compose_file=$2 + + if test_compose "$compose_file" "$test_name"; then + passed_tests+=("$test_name") + else + failed_tests+=("$test_name") + fi +} + +# Main testing routine +main() { + export DOCKER_ENABLE_SECURITY=false + ./gradlew clean build + + # Building Docker images + docker build --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-local -f ./Dockerfile . + docker build --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-lite-local -f ./Dockerfile-lite . + docker build --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-ultra-lite-local -f ./Dockerfile-ultra-lite . + + # Test each configuration + run_tests "Stirling-PDF-Ultra-Lite" "./exampleYmlFiles/docker-compose-latest-ultra-lite.yml" + run_tests "Stirling-PDF-Lite" "./exampleYmlFiles/docker-compose-latest-lite.yml" + run_tests "Stirling-PDF" "./exampleYmlFiles/docker-compose-latest.yml" + + export DOCKER_ENABLE_SECURITY=true + ./gradlew clean build + + # Building Docker images with security enabled + docker build --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-local -f ./Dockerfile . + docker build --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-lite-local -f ./Dockerfile-lite . + docker build --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-ultra-lite-local -f ./Dockerfile-ultra-lite . + + # Test each configuration with security + run_tests "Stirling-PDF-Ultra-Lite-Security" "./exampleYmlFiles/docker-compose-latest-ultra-lite-security.yml" + run_tests "Stirling-PDF-Lite-Security" "./exampleYmlFiles/docker-compose-latest-lite-security.yml" + run_tests "Stirling-PDF-Security" "./exampleYmlFiles/docker-compose-latest-security.yml" + + # Report results + echo "All tests completed." + + + + if [ ${#passed_tests[@]} -ne 0 ]; then + echo "Passed tests:" + fi + for test in "${passed_tests[@]}"; do + echo -e "\e[32m$test\e[0m" # Green color for passed tests + done + + if [ ${#failed_tests[@]} -ne 0 ]; then + echo "Failed tests:" + fi + for test in "${failed_tests[@]}"; do + echo -e "\e[31m$test\e[0m" # Red color for failed tests + done + + # Check if there are any failed tests and exit with an error code if so + if [ ${#failed_tests[@]} -ne 0 ]; then + echo "Some tests failed." + exit 1 + else + echo "All tests passed successfully." + exit 0 + fi + +} + +main From 5f771b785130154ed47952635b7acef371ffe0ec Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 30 Dec 2023 19:11:27 +0000 Subject: [PATCH 08/47] formatting --- .../software/SPDF/LibreOfficeListener.java | 38 +- .../software/SPDF/SPdfApplication.java | 28 +- .../software/SPDF/config/AppConfig.java | 38 +- .../stirling/software/SPDF/config/Beans.java | 21 +- .../SPDF/config/CleanUrlInterceptor.java | 98 +-- .../SPDF/config/ConfigInitializer.java | 198 ++--- .../SPDF/config/EndpointConfiguration.java | 60 +- .../SPDF/config/EndpointInterceptor.java | 8 +- .../software/SPDF/config/MetricsConfig.java | 3 +- .../software/SPDF/config/MetricsFilter.java | 64 +- .../software/SPDF/config/OpenApiConfig.java | 59 +- .../config/StartupApplicationListener.java | 2 - .../software/SPDF/config/WebMvcConfig.java | 7 +- .../config/YamlPropertySourceFactory.java | 10 +- .../CustomAuthenticationFailureHandler.java | 38 +- .../CustomAuthenticationSuccessHandler.java | 29 +- .../security/CustomUserDetailsService.java | 39 +- .../config/security/FirstLoginFilter.java | 25 +- .../config/security/IPRateLimitingFilter.java | 71 +- .../config/security/InitialSecuritySetup.java | 123 +-- .../config/security/LoginAttemptService.java | 42 +- .../security/RateLimitResetScheduler.java | 3 +- .../security/SecurityConfiguration.java | 156 ++-- .../security/UserAuthenticationFilter.java | 81 +- .../security/UserBasedRateLimitingFilter.java | 53 +- .../SPDF/config/security/UserService.java | 105 ++- .../SPDF/controller/api/CropController.java | 88 ++- .../SPDF/controller/api/MergeController.java | 72 +- .../api/MultiPageLayoutController.java | 160 ++-- .../controller/api/PdfOverlayController.java | 83 +- .../api/RearrangePagesPDFController.java | 344 +++++---- .../controller/api/RotationController.java | 17 +- .../controller/api/ScalePagesController.java | 126 +-- .../controller/api/SplitPDFController.java | 28 +- .../api/SplitPdfBySectionsController.java | 37 +- .../api/SplitPdfBySizeController.java | 40 +- .../api/ToSinglePageController.java | 93 +-- .../SPDF/controller/api/UserController.java | 204 ++--- .../api/converters/ConvertHtmlToPDF.java | 50 +- .../converters/ConvertImgPDFController.java | 47 +- .../api/converters/ConvertMarkdownToPdf.java | 28 +- .../converters/ConvertOfficeController.java | 41 +- .../api/converters/ConvertPDFToOffice.java | 102 ++- .../api/converters/ConvertPDFToPDFA.java | 24 +- .../api/converters/ConvertWebsiteToPDF.java | 89 +-- .../api/converters/ExtractController.java | 81 +- .../api/filters/FilterController.java | 302 ++++---- .../api/misc/AutoRenameController.java | 156 ++-- .../api/misc/AutoSplitPdfController.java | 41 +- .../api/misc/BlankPageController.java | 68 +- .../api/misc/CompressController.java | 127 +-- .../api/misc/ExtractImageScansController.java | 95 ++- .../api/misc/ExtractImagesController.java | 44 +- .../api/misc/FakeScanControllerWIP.java | 158 ++-- .../api/misc/MetadataController.java | 47 +- .../controller/api/misc/OCRController.java | 93 ++- .../api/misc/OverlayImageController.java | 11 +- .../api/misc/PageNumbersController.java | 61 +- .../controller/api/misc/RepairController.java | 21 +- .../controller/api/misc/ShowJavascript.java | 63 +- .../api/pipeline/ApiDocService.java | 85 +- .../api/pipeline/PipelineController.java | 131 ++-- .../pipeline/PipelineDirectoryProcessor.java | 162 ++-- .../api/pipeline/PipelineProcessor.java | 480 ++++++------ .../api/pipeline/UserServiceInterface.java | 1 + .../api/security/CertSignController.java | 365 +++++---- .../controller/api/security/GetInfoOnPDF.java | 384 +++++----- .../api/security/PasswordController.java | 47 +- .../api/security/RedactController.java | 58 +- .../api/security/SanitizeController.java | 154 ++-- .../api/security/WatermarkController.java | 285 ++++--- .../api/strippers/PDFTableStripper.java | 194 ++--- .../controller/web/AccountWebController.java | 211 ++--- .../web/ConverterWebController.java | 14 +- .../controller/web/GeneralWebController.java | 229 +++--- .../controller/web/HomeWebController.java | 14 +- .../controller/web/MetricsController.java | 164 ++-- .../controller/web/OtherWebController.java | 38 +- .../controller/web/SecurityWebController.java | 13 +- .../software/SPDF/model/ApiEndpoint.java | 26 +- .../SPDF/model/ApiKeyAuthenticationToken.java | 9 +- .../SPDF/model/ApplicationProperties.java | 725 +++++++++--------- .../software/SPDF/model/AttemptCounter.java | 1 + .../software/SPDF/model/Authority.java | 56 +- .../stirling/software/SPDF/model/PDFText.java | 3 +- .../software/SPDF/model/PersistentLogin.java | 49 +- .../software/SPDF/model/PipelineConfig.java | 4 +- .../SPDF/model/PipelineOperation.java | 41 +- .../stirling/software/SPDF/model/Role.java | 17 +- .../software/SPDF/model/SortTypes.java | 12 +- .../stirling/software/SPDF/model/User.java | 133 ++-- .../software/SPDF/model/api/GeneralFile.java | 5 +- .../SPDF/model/api/HandleDataRequest.java | 1 + .../software/SPDF/model/api/ImageFile.java | 3 +- .../SPDF/model/api/MultiplePDFFiles.java | 4 +- .../SPDF/model/api/PDFComparison.java | 12 +- .../SPDF/model/api/PDFComparisonAndCount.java | 8 +- .../software/SPDF/model/api/PDFFile.java | 4 +- .../model/api/PDFWithImageFormatRequest.java | 8 +- .../SPDF/model/api/PDFWithPageNums.java | 50 +- .../SPDF/model/api/PDFWithPageSize.java | 13 +- .../model/api/SplitPdfBySectionsRequest.java | 6 +- .../api/converters/ConvertToImageRequest.java | 16 +- .../api/converters/ConvertToPdfRequest.java | 16 +- .../converters/PdfToPresentationRequest.java | 7 +- .../api/converters/PdfToTextOrRTFRequest.java | 7 +- .../api/converters/PdfToWordRequest.java | 7 +- .../model/api/converters/UrlToPdfRequest.java | 1 + .../SPDF/model/api/extract/PDFFilePage.java | 7 +- .../model/api/filter/ContainsTextRequest.java | 3 +- .../model/api/filter/FileSizeRequest.java | 5 +- .../model/api/filter/PageRotationRequest.java | 4 +- .../model/api/filter/PageSizeRequest.java | 5 +- .../SPDF/model/api/general/CropPdfForm.java | 14 +- .../general/MergeMultiplePagesRequest.java | 11 +- .../model/api/general/MergePdfsRequest.java | 18 +- .../model/api/general/OverlayPdfsRequest.java | 20 +- .../api/general/RearrangePagesRequest.java | 25 +- .../model/api/general/RotatePDFRequest.java | 8 +- .../model/api/general/ScalePagesRequest.java | 7 +- .../general/SplitPdfBySizeOrCountRequest.java | 18 +- .../model/api/misc/AddPageNumbersRequest.java | 11 +- .../model/api/misc/AutoSplitPdfRequest.java | 9 +- .../model/api/misc/ExtractHeaderRequest.java | 9 +- .../api/misc/ExtractImageScansRequest.java | 28 +- .../SPDF/model/api/misc/MetadataRequest.java | 7 +- .../model/api/misc/OptimizePdfRequest.java | 9 +- .../model/api/misc/OverlayImageRequest.java | 15 +- .../api/misc/ProcessPdfWithOcrRequest.java | 12 +- .../api/misc/RemoveBlankPagesRequest.java | 13 +- .../api/security/AddPasswordRequest.java | 26 +- .../api/security/AddWatermarkRequest.java | 15 +- .../api/security/PDFPasswordRequest.java | 3 +- .../model/api/security/RedactPdfRequest.java | 3 +- .../api/security/SanitizePdfRequest.java | 3 +- .../api/security/SignPDFWithCertRequest.java | 15 +- .../software/SPDF/pdf/ImageFinder.java | 131 ++-- .../software/SPDF/pdf/TextFinder.java | 135 ++-- .../SPDF/repository/AuthorityRepository.java | 4 +- .../repository/JPATokenRepositoryImpl.java | 6 +- .../repository/PersistentLoginRepository.java | 3 +- .../SPDF/repository/UserRepository.java | 2 +- .../software/SPDF/utils/ErrorUtils.java | 1 - .../software/SPDF/utils/FileToPdf.java | 151 ++-- .../software/SPDF/utils/GeneralUtils.java | 267 ++++--- .../SPDF/utils/ImageProcessingUtils.java | 43 +- .../SPDF/utils/PDFManipulationUtils.java | 4 +- .../software/SPDF/utils/PDFToFile.java | 45 +- .../software/SPDF/utils/PdfUtils.java | 171 +++-- .../software/SPDF/utils/ProcessExecutor.java | 147 ++-- .../software/SPDF/utils/PropertyConfigs.java | 55 +- .../software/SPDF/utils/RequestUriUtils.java | 18 +- .../software/SPDF/utils/WebResponseUtils.java | 79 +- .../resources/static/pdfjs/cmaps/CNS2-V.bcmap | 4 +- .../static/pdfjs/cmaps/ETenms-B5-H.bcmap | 4 +- 155 files changed, 5539 insertions(+), 4767 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/LibreOfficeListener.java b/src/main/java/stirling/software/SPDF/LibreOfficeListener.java index 7f4fc160..6d32adc3 100644 --- a/src/main/java/stirling/software/SPDF/LibreOfficeListener.java +++ b/src/main/java/stirling/software/SPDF/LibreOfficeListener.java @@ -22,14 +22,14 @@ public class LibreOfficeListener { private Process process; - private LibreOfficeListener() { - } + private LibreOfficeListener() {} private boolean isListenerRunning() { try { System.out.println("waiting for listener to start"); Socket socket = new Socket(); - socket.connect(new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second + socket.connect( + new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second socket.close(); return true; } catch (IOException e) { @@ -49,21 +49,22 @@ public class LibreOfficeListener { // Start a background thread to monitor the activity timeout executorService = Executors.newSingleThreadExecutor(); - executorService.submit(() -> { - while (true) { - long idleTime = System.currentTimeMillis() - lastActivityTime; - if (idleTime >= ACTIVITY_TIMEOUT) { - // If there has been no activity for too long, tear down the listener - process.destroy(); - break; - } - try { - Thread.sleep(5000); // Check for inactivity every 5 seconds - } catch (InterruptedException e) { - break; - } - } - }); + executorService.submit( + () -> { + while (true) { + long idleTime = System.currentTimeMillis() - lastActivityTime; + if (idleTime >= ACTIVITY_TIMEOUT) { + // If there has been no activity for too long, tear down the listener + process.destroy(); + break; + } + try { + Thread.sleep(5000); // Check for inactivity every 5 seconds + } catch (InterruptedException e) { + break; + } + } + }); // Wait for the listener to start up long startTime = System.currentTimeMillis(); @@ -92,5 +93,4 @@ public class LibreOfficeListener { process.destroy(); } } - } diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java index a7ce7f14..ef3733c6 100644 --- a/src/main/java/stirling/software/SPDF/SPdfApplication.java +++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java @@ -13,13 +13,12 @@ import org.springframework.scheduling.annotation.EnableScheduling; import jakarta.annotation.PostConstruct; import stirling.software.SPDF.config.ConfigInitializer; import stirling.software.SPDF.utils.GeneralUtils; -@SpringBootApplication +@SpringBootApplication @EnableScheduling public class SPdfApplication { - @Autowired - private Environment env; + @Autowired private Environment env; @PostConstruct public void init() { @@ -44,21 +43,24 @@ public class SPdfApplication { } public static void main(String[] args) { - SpringApplication app = new SpringApplication(SPdfApplication.class); - app.addInitializers(new ConfigInitializer()); - if (Files.exists(Paths.get("configs/settings.yml"))) { - app.setDefaultProperties(Collections.singletonMap("spring.config.additional-location", "file:configs/settings.yml")); + SpringApplication app = new SpringApplication(SPdfApplication.class); + app.addInitializers(new ConfigInitializer()); + if (Files.exists(Paths.get("configs/settings.yml"))) { + app.setDefaultProperties( + Collections.singletonMap( + "spring.config.additional-location", "file:configs/settings.yml")); } else { - System.out.println("External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead."); + System.out.println( + "External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead."); } app.run(args); try { - Thread.sleep(1000); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } + Thread.sleep(1000); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } GeneralUtils.createDir("customFiles/static/"); GeneralUtils.createDir("customFiles/templates/"); diff --git a/src/main/java/stirling/software/SPDF/config/AppConfig.java b/src/main/java/stirling/software/SPDF/config/AppConfig.java index faf85b28..273de957 100644 --- a/src/main/java/stirling/software/SPDF/config/AppConfig.java +++ b/src/main/java/stirling/software/SPDF/config/AppConfig.java @@ -5,13 +5,12 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import stirling.software.SPDF.model.ApplicationProperties; + @Configuration public class AppConfig { - - @Autowired - ApplicationProperties applicationProperties; - + @Autowired ApplicationProperties applicationProperties; + @Bean(name = "loginEnabled") public boolean loginEnabled() { return applicationProperties.getSecurity().getEnableLogin(); @@ -19,7 +18,7 @@ public class AppConfig { @Bean(name = "appName") public String appName() { - String homeTitle = applicationProperties.getUi().getAppName(); + String homeTitle = applicationProperties.getUi().getAppName(); return (homeTitle != null) ? homeTitle : "Stirling PDF"; } @@ -31,28 +30,31 @@ public class AppConfig { @Bean(name = "homeText") public String homeText() { - return (applicationProperties.getUi().getHomeDescription() != null) ? applicationProperties.getUi().getHomeDescription() : "null"; + return (applicationProperties.getUi().getHomeDescription() != null) + ? applicationProperties.getUi().getHomeDescription() + : "null"; } - @Bean(name = "navBarText") public String navBarText() { - String defaultNavBar = applicationProperties.getUi().getAppNameNavbar() != null ? applicationProperties.getUi().getAppNameNavbar() : applicationProperties.getUi().getAppName(); + String defaultNavBar = + applicationProperties.getUi().getAppNameNavbar() != null + ? applicationProperties.getUi().getAppNameNavbar() + : applicationProperties.getUi().getAppName(); return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF"; } - + @Bean(name = "enableAlphaFunctionality") - public boolean enableAlphaFunctionality() { - return applicationProperties.getSystem().getEnableAlphaFunctionality() != null ? applicationProperties.getSystem().getEnableAlphaFunctionality() : false; + public boolean enableAlphaFunctionality() { + return applicationProperties.getSystem().getEnableAlphaFunctionality() != null + ? applicationProperties.getSystem().getEnableAlphaFunctionality() + : false; } - - @Bean(name = "rateLimit") + + @Bean(name = "rateLimit") public boolean rateLimit() { String appName = System.getProperty("rateLimit"); - if (appName == null) - appName = System.getenv("rateLimit"); + if (appName == null) appName = System.getenv("rateLimit"); return (appName != null) ? Boolean.valueOf(appName) : false; } - - -} \ 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 d1c6a03b..9230a0a0 100644 --- a/src/main/java/stirling/software/SPDF/config/Beans.java +++ b/src/main/java/stirling/software/SPDF/config/Beans.java @@ -15,10 +15,9 @@ import stirling.software.SPDF.model.ApplicationProperties; @Configuration public class Beans implements WebMvcConfigurer { - - @Autowired - ApplicationProperties applicationProperties; - + + @Autowired ApplicationProperties applicationProperties; + @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(localeChangeInterceptor()); @@ -35,25 +34,26 @@ public class Beans implements WebMvcConfigurer { @Bean public LocaleResolver localeResolver() { SessionLocaleResolver slr = new SessionLocaleResolver(); - - + String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale(); - Locale defaultLocale = Locale.UK; // Fallback to UK locale if environment variable is not set + 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)) { + if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { defaultLocale = tempLocale; } else { - tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_","-")); + 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."); + System.err.println( + "Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK."); } } } @@ -61,5 +61,4 @@ public class Beans implements WebMvcConfigurer { slr.setDefaultLocale(defaultLocale); return slr; } - } diff --git a/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java b/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java index 894d50d8..472fb951 100644 --- a/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java +++ b/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java @@ -13,56 +13,62 @@ import jakarta.servlet.http.HttpServletResponse; public class CleanUrlInterceptor implements HandlerInterceptor { - private static final List ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error", "file", "messageType"); + private static final List ALLOWED_PARAMS = + Arrays.asList( + "lang", "endpoint", "endpoints", "logout", "error", "file", "messageType"); - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { - String queryString = request.getQueryString(); - if (queryString != null && !queryString.isEmpty()) { - String requestURI = request.getRequestURI(); - Map parameters = new HashMap<>(); + @Override + public boolean preHandle( + HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + String queryString = request.getQueryString(); + if (queryString != null && !queryString.isEmpty()) { + String requestURI = request.getRequestURI(); + Map parameters = new HashMap<>(); - // Keep only the allowed parameters - String[] queryParameters = queryString.split("&"); - for (String param : queryParameters) { - String[] keyValue = param.split("="); - if (keyValue.length != 2) { - continue; - } - if (ALLOWED_PARAMS.contains(keyValue[0])) { - parameters.put(keyValue[0], keyValue[1]); - } - } + // Keep only the allowed parameters + String[] queryParameters = queryString.split("&"); + for (String param : queryParameters) { + String[] keyValue = param.split("="); + if (keyValue.length != 2) { + continue; + } + if (ALLOWED_PARAMS.contains(keyValue[0])) { + parameters.put(keyValue[0], keyValue[1]); + } + } - // If there are any parameters that are not allowed - if (parameters.size() != queryParameters.length) { - // Construct new query string - StringBuilder newQueryString = new StringBuilder(); - for (Map.Entry entry : parameters.entrySet()) { - if (newQueryString.length() > 0) { - newQueryString.append("&"); - } - newQueryString.append(entry.getKey()).append("=").append(entry.getValue()); - } + // If there are any parameters that are not allowed + if (parameters.size() != queryParameters.length) { + // Construct new query string + StringBuilder newQueryString = new StringBuilder(); + for (Map.Entry entry : parameters.entrySet()) { + if (newQueryString.length() > 0) { + newQueryString.append("&"); + } + newQueryString.append(entry.getKey()).append("=").append(entry.getValue()); + } - // Redirect to the URL with only allowed query parameters - String redirectUrl = requestURI + "?" + newQueryString; - response.sendRedirect(redirectUrl); - return false; - } - } - return true; - } + // Redirect to the URL with only allowed query parameters + String redirectUrl = requestURI + "?" + newQueryString; + response.sendRedirect(redirectUrl); + return false; + } + } + return true; + } - @Override - public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, - ModelAndView modelAndView) { - } + @Override + public void postHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler, + ModelAndView modelAndView) {} - @Override - public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, - Exception ex) { - } + @Override + public void afterCompletion( + HttpServletRequest request, + HttpServletResponse response, + Object handler, + Exception ex) {} } diff --git a/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java b/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java index 862f5718..6435c955 100644 --- a/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java +++ b/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java @@ -19,111 +19,125 @@ import java.util.stream.Collectors; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; -public class ConfigInitializer implements ApplicationContextInitializer { +public class ConfigInitializer + implements ApplicationContextInitializer { - @Override - public void initialize(ConfigurableApplicationContext applicationContext) { - try { - ensureConfigExists(); - } catch (IOException e) { - throw new RuntimeException("Failed to initialize application configuration", e); - } - } + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + try { + ensureConfigExists(); + } catch (IOException e) { + throw new RuntimeException("Failed to initialize application configuration", e); + } + } - public void ensureConfigExists() throws IOException { - // Define the path to the external config directory - Path destPath = Paths.get("configs", "settings.yml"); + public void ensureConfigExists() throws IOException { + // Define the path to the external config directory + Path destPath = Paths.get("configs", "settings.yml"); - // Check if the file already exists - if (Files.notExists(destPath)) { - // Ensure the destination directory exists - Files.createDirectories(destPath.getParent()); + // Check if the file already exists + if (Files.notExists(destPath)) { + // Ensure the destination directory exists + Files.createDirectories(destPath.getParent()); - // Copy the resource from classpath to the external directory - try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) { - if (in != null) { - Files.copy(in, destPath); - } else { - throw new FileNotFoundException("Resource file not found: settings.yml.template"); - } - } - } else { - // If user file exists, we need to merge it with the template from the classpath - List templateLines; - try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) { - templateLines = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)).lines() - .collect(Collectors.toList()); - } + // Copy the resource from classpath to the external directory + try (InputStream in = + getClass().getClassLoader().getResourceAsStream("settings.yml.template")) { + if (in != null) { + Files.copy(in, destPath); + } else { + throw new FileNotFoundException( + "Resource file not found: settings.yml.template"); + } + } + } else { + // If user file exists, we need to merge it with the template from the classpath + List templateLines; + try (InputStream in = + getClass().getClassLoader().getResourceAsStream("settings.yml.template")) { + templateLines = + new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.toList()); + } - mergeYamlFiles(templateLines, destPath, destPath); - } - } + mergeYamlFiles(templateLines, destPath, destPath); + } + } - public void mergeYamlFiles(List templateLines, Path userFilePath, Path outputPath) throws IOException { - List userLines = Files.readAllLines(userFilePath); - List mergedLines = new ArrayList<>(); - boolean insideAutoGenerated = false; - boolean beforeFirstKey = true; + public void mergeYamlFiles(List templateLines, Path userFilePath, Path outputPath) + throws IOException { + List userLines = Files.readAllLines(userFilePath); + List mergedLines = new ArrayList<>(); + boolean insideAutoGenerated = false; + boolean beforeFirstKey = true; - Function isCommented = line -> line.trim().startsWith("#"); - Function extractKey = line -> { - String[] parts = line.split(":"); - return parts.length > 0 ? parts[0].trim().replace("#", "").trim() : ""; - }; + Function isCommented = line -> line.trim().startsWith("#"); + Function extractKey = + line -> { + String[] parts = line.split(":"); + return parts.length > 0 ? parts[0].trim().replace("#", "").trim() : ""; + }; - Set userKeys = userLines.stream().map(extractKey).collect(Collectors.toSet()); + Set userKeys = userLines.stream().map(extractKey).collect(Collectors.toSet()); - for (String line : templateLines) { - String key = extractKey.apply(line); + for (String line : templateLines) { + String key = extractKey.apply(line); - if (line.trim().equalsIgnoreCase("AutomaticallyGenerated:")) { - insideAutoGenerated = true; - mergedLines.add(line); - continue; - } else if (insideAutoGenerated && line.trim().isEmpty()) { - insideAutoGenerated = false; - mergedLines.add(line); - continue; - } + if (line.trim().equalsIgnoreCase("AutomaticallyGenerated:")) { + insideAutoGenerated = true; + mergedLines.add(line); + continue; + } else if (insideAutoGenerated && line.trim().isEmpty()) { + insideAutoGenerated = false; + mergedLines.add(line); + continue; + } - if (beforeFirstKey && (isCommented.apply(line) || line.trim().isEmpty())) { - // Handle top comments and empty lines before the first key. - mergedLines.add(line); - continue; - } + if (beforeFirstKey && (isCommented.apply(line) || line.trim().isEmpty())) { + // Handle top comments and empty lines before the first key. + mergedLines.add(line); + continue; + } - if (!key.isEmpty()) - beforeFirstKey = false; + if (!key.isEmpty()) beforeFirstKey = false; - if (userKeys.contains(key)) { - // If user has any version (commented or uncommented) of this key, skip the - // template line - Optional userValue = userLines.stream() - .filter(l -> extractKey.apply(l).equalsIgnoreCase(key) && !isCommented.apply(l)).findFirst(); - if (userValue.isPresent()) - mergedLines.add(userValue.get()); - continue; - } + if (userKeys.contains(key)) { + // If user has any version (commented or uncommented) of this key, skip the + // template line + Optional userValue = + userLines.stream() + .filter( + l -> + extractKey.apply(l).equalsIgnoreCase(key) + && !isCommented.apply(l)) + .findFirst(); + if (userValue.isPresent()) mergedLines.add(userValue.get()); + continue; + } - if (isCommented.apply(line) || line.trim().isEmpty() || !userKeys.contains(key)) { - mergedLines.add(line); // If line is commented, empty or key not present in user's file, retain the - // template line - continue; - } - } + if (isCommented.apply(line) || line.trim().isEmpty() || !userKeys.contains(key)) { + mergedLines.add( + line); // If line is commented, empty or key not present in user's file, + // retain the + // template line + continue; + } + } - // Add any additional uncommented user lines that are not present in the - // template - for (String userLine : userLines) { - String userKey = extractKey.apply(userLine); - boolean isPresentInTemplate = templateLines.stream().map(extractKey) - .anyMatch(templateKey -> templateKey.equalsIgnoreCase(userKey)); - if (!isPresentInTemplate && !isCommented.apply(userLine)) { - mergedLines.add(userLine); - } - } + // Add any additional uncommented user lines that are not present in the + // template + for (String userLine : userLines) { + String userKey = extractKey.apply(userLine); + boolean isPresentInTemplate = + templateLines.stream() + .map(extractKey) + .anyMatch(templateKey -> templateKey.equalsIgnoreCase(userKey)); + if (!isPresentInTemplate && !isCommented.apply(userLine)) { + mergedLines.add(userLine); + } + } - Files.write(outputPath, mergedLines, StandardCharsets.UTF_8); - } - -} \ No newline at end of file + Files.write(outputPath, mergedLines, StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index ebba9815..ddba4623 100644 --- a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -12,6 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import stirling.software.SPDF.model.ApplicationProperties; + @Service public class EndpointConfiguration { private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class); @@ -26,16 +27,16 @@ public class EndpointConfiguration { init(); processEnvironmentConfigs(); } - + public void enableEndpoint(String endpoint) { - endpointStatuses.put(endpoint, true); + 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); - } + if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) { + logger.info("Disabling {}", endpoint); + endpointStatuses.put(endpoint, false); + } } public boolean isEndpointEnabled(String endpoint) { @@ -66,7 +67,7 @@ public class EndpointConfiguration { } } } - + public void init() { // Adding endpoints to "PageOps" group addEndpointToGroup("PageOps", "remove-pages"); @@ -84,8 +85,7 @@ public class EndpointConfiguration { addEndpointToGroup("PageOps", "split-by-size-or-count"); addEndpointToGroup("PageOps", "overlay-pdf"); addEndpointToGroup("PageOps", "split-pdf-by-sections"); - - + // Adding endpoints to "Convert" group addEndpointToGroup("Convert", "pdf-to-img"); addEndpointToGroup("Convert", "img-to-pdf"); @@ -101,8 +101,7 @@ public class EndpointConfiguration { addEndpointToGroup("Convert", "url-to-pdf"); addEndpointToGroup("Convert", "markdown-to-pdf"); addEndpointToGroup("Convert", "pdf-to-csv"); - - + // Adding endpoints to "Security" group addEndpointToGroup("Security", "add-password"); addEndpointToGroup("Security", "remove-password"); @@ -111,8 +110,7 @@ public class EndpointConfiguration { addEndpointToGroup("Security", "cert-sign"); addEndpointToGroup("Security", "sanitize-pdf"); addEndpointToGroup("Security", "auto-redact"); - - + // Adding endpoints to "Other" group addEndpointToGroup("Other", "ocr-pdf"); addEndpointToGroup("Other", "add-image"); @@ -130,10 +128,8 @@ public class EndpointConfiguration { addEndpointToGroup("Other", "auto-rename"); addEndpointToGroup("Other", "get-info-on-pdf"); addEndpointToGroup("Other", "show-javascript"); - - - - //CLI + + // CLI addEndpointToGroup("CLI", "compress-pdf"); addEndpointToGroup("CLI", "extract-image-scans"); addEndpointToGroup("CLI", "remove-blanks"); @@ -149,19 +145,18 @@ public class EndpointConfiguration { addEndpointToGroup("CLI", "ocr-pdf"); addEndpointToGroup("CLI", "html-to-pdf"); addEndpointToGroup("CLI", "url-to-pdf"); - - - //python + + // python addEndpointToGroup("Python", "extract-image-scans"); addEndpointToGroup("Python", "remove-blanks"); addEndpointToGroup("Python", "html-to-pdf"); addEndpointToGroup("Python", "url-to-pdf"); - - //openCV + + // openCV addEndpointToGroup("OpenCV", "extract-image-scans"); addEndpointToGroup("OpenCV", "remove-blanks"); - //LibreOffice + // LibreOffice addEndpointToGroup("LibreOffice", "repair"); addEndpointToGroup("LibreOffice", "file-to-pdf"); addEndpointToGroup("LibreOffice", "xlsx-to-pdf"); @@ -170,14 +165,13 @@ public class EndpointConfiguration { addEndpointToGroup("LibreOffice", "pdf-to-text"); addEndpointToGroup("LibreOffice", "pdf-to-html"); addEndpointToGroup("LibreOffice", "pdf-to-xml"); - - - //OCRmyPDF + + // OCRmyPDF addEndpointToGroup("OCRmyPDF", "compress-pdf"); addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa"); addEndpointToGroup("OCRmyPDF", "ocr-pdf"); - - //Java + + // Java addEndpointToGroup("Java", "merge-pdfs"); addEndpointToGroup("Java", "remove-pages"); addEndpointToGroup("Java", "split-pdfs"); @@ -210,16 +204,14 @@ public class EndpointConfiguration { addEndpointToGroup("Java", "split-by-size-or-count"); addEndpointToGroup("Java", "overlay-pdf"); addEndpointToGroup("Java", "split-pdf-by-sections"); - - //Javascript + + // Javascript addEndpointToGroup("Javascript", "pdf-organizer"); addEndpointToGroup("Javascript", "sign"); addEndpointToGroup("Javascript", "compare"); addEndpointToGroup("Javascript", "adjust-contrast"); - - } - + private void processEnvironmentConfigs() { List endpointsToRemove = applicationProperties.getEndpoints().getToRemove(); List groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove(); @@ -236,6 +228,4 @@ public class EndpointConfiguration { } } } - } - diff --git a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java index 77191a41..d408b9ea 100644 --- a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java +++ b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java @@ -10,11 +10,11 @@ import jakarta.servlet.http.HttpServletResponse; @Component public class EndpointInterceptor implements HandlerInterceptor { - @Autowired - private EndpointConfiguration endpointConfiguration; + @Autowired private EndpointConfiguration endpointConfiguration; @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + public boolean preHandle( + HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestURI = request.getRequestURI(); if (!endpointConfiguration.isEndpointEnabled(requestURI)) { @@ -23,4 +23,4 @@ public class EndpointInterceptor implements HandlerInterceptor { } return true; } -} \ No newline at end of file +} diff --git a/src/main/java/stirling/software/SPDF/config/MetricsConfig.java b/src/main/java/stirling/software/SPDF/config/MetricsConfig.java index 1cdc99e3..3877c566 100644 --- a/src/main/java/stirling/software/SPDF/config/MetricsConfig.java +++ b/src/main/java/stirling/software/SPDF/config/MetricsConfig.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.config; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -21,4 +22,4 @@ public class MetricsConfig { } }; } -} \ No newline at end of file +} diff --git a/src/main/java/stirling/software/SPDF/config/MetricsFilter.java b/src/main/java/stirling/software/SPDF/config/MetricsFilter.java index 9abb68bf..9207fd07 100644 --- a/src/main/java/stirling/software/SPDF/config/MetricsFilter.java +++ b/src/main/java/stirling/software/SPDF/config/MetricsFilter.java @@ -8,6 +8,7 @@ import org.springframework.web.filter.OncePerRequestFilter; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; + import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -16,35 +17,48 @@ import jakarta.servlet.http.HttpServletResponse; @Component public class MetricsFilter extends OncePerRequestFilter { - private final MeterRegistry meterRegistry; + private final MeterRegistry meterRegistry; - @Autowired - public MetricsFilter(MeterRegistry meterRegistry) { - this.meterRegistry = meterRegistry; - } + @Autowired + public MetricsFilter(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + } - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - String uri = request.getRequestURI(); + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String uri = request.getRequestURI(); - // System.out.println("uri="+uri + ", method=" + request.getMethod() ); - // Ignore static resources - if (!(uri.startsWith("/js") || uri.startsWith("/v1/api-docs") || uri.endsWith("robots.txt") - || uri.startsWith("/images") || uri.startsWith("/images")|| uri.endsWith(".png") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".map") - || uri.endsWith(".svg") || uri.endsWith(".js") || uri.contains("swagger") - || uri.startsWith("/api/v1/info") || uri.startsWith("/site.webmanifest") || uri.startsWith("/fonts") || uri.startsWith("/pdfjs") )) { - - - - Counter counter = Counter.builder("http.requests").tag("uri", uri).tag("method", request.getMethod()) - .register(meterRegistry); + // System.out.println("uri="+uri + ", method=" + request.getMethod() ); + // Ignore static resources + if (!(uri.startsWith("/js") + || uri.startsWith("/v1/api-docs") + || uri.endsWith("robots.txt") + || uri.startsWith("/images") + || uri.startsWith("/images") + || uri.endsWith(".png") + || uri.endsWith(".ico") + || uri.endsWith(".css") + || uri.endsWith(".map") + || uri.endsWith(".svg") + || uri.endsWith(".js") + || uri.contains("swagger") + || uri.startsWith("/api/v1/info") + || uri.startsWith("/site.webmanifest") + || uri.startsWith("/fonts") + || uri.startsWith("/pdfjs"))) { - counter.increment(); - // System.out.println("Counted"); - } + Counter counter = + Counter.builder("http.requests") + .tag("uri", uri) + .tag("method", request.getMethod()) + .register(meterRegistry); - filterChain.doFilter(request, response); - } + counter.increment(); + // System.out.println("Counted"); + } + filterChain.doFilter(request, response); + } } diff --git a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java index 5dba40d0..a852bb1a 100644 --- a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java +++ b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java @@ -9,34 +9,45 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; + import stirling.software.SPDF.model.ApplicationProperties; @Configuration public class OpenApiConfig { - @Autowired - ApplicationProperties applicationProperties; + @Autowired ApplicationProperties applicationProperties; - @Bean - public OpenAPI customOpenAPI() { - String version = getClass().getPackage().getImplementationVersion(); - if (version == null) { - version = "1.0.0"; // default version if all else fails - } + @Bean + public OpenAPI customOpenAPI() { + String version = getClass().getPackage().getImplementationVersion(); + if (version == null) { + version = "1.0.0"; // default version if all else fails + } - SecurityScheme apiKeyScheme = new SecurityScheme().type(SecurityScheme.Type.APIKEY).in(SecurityScheme.In.HEADER) - .name("X-API-KEY"); - if (!applicationProperties.getSecurity().getEnableLogin()) { - return new OpenAPI().components(new Components()) - .info(new Info().title("Stirling PDF API").version(version).description( - "API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")); - } else { - return new OpenAPI().components(new Components().addSecuritySchemes("apiKey", apiKeyScheme)) - .info(new Info().title("Stirling PDF API").version(version).description( - "API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")) - .addSecurityItem(new SecurityRequirement().addList("apiKey")); - } - - } - -} \ No newline at end of file + SecurityScheme apiKeyScheme = + new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .name("X-API-KEY"); + if (!applicationProperties.getSecurity().getEnableLogin()) { + return new OpenAPI() + .components(new Components()) + .info( + new Info() + .title("Stirling PDF API") + .version(version) + .description( + "API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")); + } else { + return new OpenAPI() + .components(new Components().addSecuritySchemes("apiKey", apiKeyScheme)) + .info( + new Info() + .title("Stirling PDF API") + .version(version) + .description( + "API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")) + .addSecurityItem(new SecurityRequirement().addList("apiKey")); + } + } +} diff --git a/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java b/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java index 77b69c88..07644b30 100644 --- a/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java +++ b/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.config; - import java.time.LocalDateTime; import org.springframework.context.ApplicationListener; @@ -17,4 +16,3 @@ public class StartupApplicationListener implements ApplicationListener createPropertySource(String name, EncodedResource encodedResource) - throws IOException { + public PropertySource createPropertySource(String name, EncodedResource encodedResource) + throws IOException { YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); factory.setResources(encodedResource.getResource()); Properties properties = factory.getObject(); - return new PropertiesPropertySource(encodedResource.getResource().getFilename(), properties); + return new PropertiesPropertySource( + encodedResource.getResource().getFilename(), properties); } -} \ No newline at end of file +} diff --git a/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java index 397a8a70..6c2a05d3 100644 --- a/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java +++ b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java @@ -12,36 +12,38 @@ import org.springframework.stereotype.Component; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; + @Component public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { - - @Autowired - private final LoginAttemptService loginAttemptService; + + @Autowired private final LoginAttemptService loginAttemptService; @Autowired public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) { this.loginAttemptService = loginAttemptService; } - + @Override - public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) - throws IOException, ServletException { - String ip = request.getRemoteAddr(); + public void onAuthenticationFailure( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception) + throws IOException, ServletException { + String ip = request.getRemoteAddr(); logger.error("Failed login attempt from IP: " + ip); - + String username = request.getParameter("username"); - if(loginAttemptService.loginAttemptCheck(username)) { - setDefaultFailureUrl("/login?error=locked"); - + if (loginAttemptService.loginAttemptCheck(username)) { + setDefaultFailureUrl("/login?error=locked"); + } else { - if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) { - setDefaultFailureUrl("/login?error=badcredentials"); - } else if (exception.getClass().isAssignableFrom(LockedException.class)) { - setDefaultFailureUrl("/login?error=locked"); - } + if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) { + setDefaultFailureUrl("/login?error=badcredentials"); + } else if (exception.getClass().isAssignableFrom(LockedException.class)) { + setDefaultFailureUrl("/login?error=locked"); + } } - - + super.onAuthenticationFailure(request, response, exception); } } diff --git a/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationSuccessHandler.java b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationSuccessHandler.java index cd2217e1..d14466ea 100644 --- a/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationSuccessHandler.java +++ b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationSuccessHandler.java @@ -15,30 +15,33 @@ import jakarta.servlet.http.HttpSession; import stirling.software.SPDF.utils.RequestUriUtils; @Component -public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { +public class CustomAuthenticationSuccessHandler + extends SavedRequestAwareAuthenticationSuccessHandler { - @Autowired - private LoginAttemptService loginAttemptService; + @Autowired private LoginAttemptService loginAttemptService; @Override - public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { - String username = request.getParameter("username"); + public void onAuthenticationSuccess( + HttpServletRequest request, HttpServletResponse response, Authentication authentication) + throws ServletException, IOException { + String username = request.getParameter("username"); loginAttemptService.loginSucceeded(username); - - + // Get the saved request HttpSession session = request.getSession(false); - SavedRequest savedRequest = session != null ? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST") : null; - if (savedRequest != null && !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) { + SavedRequest savedRequest = + session != null + ? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST") + : null; + if (savedRequest != null + && !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) { // Redirect to the original destination super.onAuthenticationSuccess(request, response, authentication); } else { // Redirect to the root URL (considering context path) getRedirectStrategy().sendRedirect(request, response, "/"); } - - //super.onAuthenticationSuccess(request, response, authentication); - } - + // super.onAuthenticationSuccess(request, response, authentication); + } } diff --git a/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java b/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java index 77db2cd4..021cdc31 100644 --- a/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java +++ b/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java @@ -20,33 +20,38 @@ import stirling.software.SPDF.repository.UserRepository; @Service public class CustomUserDetailsService implements UserDetailsService { - @Autowired - private UserRepository userRepository; + @Autowired private UserRepository userRepository; + + @Autowired private LoginAttemptService loginAttemptService; - @Autowired - private LoginAttemptService loginAttemptService; - @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - User user = userRepository.findByUsername(username) - .orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + username)); + User user = + userRepository + .findByUsername(username) + .orElseThrow( + () -> + new UsernameNotFoundException( + "No user found with username: " + username)); if (loginAttemptService.isBlocked(username)) { - throw new LockedException("Your account has been locked due to too many failed login attempts."); + throw new LockedException( + "Your account has been locked due to too many failed login attempts."); } - + return new org.springframework.security.core.userdetails.User( - user.getUsername(), - user.getPassword(), - user.isEnabled(), - true, true, true, - getAuthorities(user.getAuthorities()) - ); + user.getUsername(), + user.getPassword(), + user.isEnabled(), + true, + true, + true, + getAuthorities(user.getAuthorities())); } private Collection getAuthorities(Set authorities) { return authorities.stream() - .map(authority -> new SimpleGrantedAuthority(authority.getAuthority())) - .collect(Collectors.toList()); + .map(authority -> new SimpleGrantedAuthority(authority.getAuthority())) + .collect(Collectors.toList()); } } diff --git a/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java b/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java index 65acf148..b272327a 100644 --- a/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java @@ -19,16 +19,16 @@ import stirling.software.SPDF.utils.RequestUriUtils; @Component public class FirstLoginFilter extends OncePerRequestFilter { - - @Autowired - @Lazy - private UserService userService; - + + @Autowired @Lazy private UserService userService; + @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - String method = request.getMethod(); - String requestURI = request.getRequestURI(); - // Check if the request is for static resources + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String method = request.getMethod(); + String requestURI = request.getRequestURI(); + // Check if the request is for static resources boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI); // If it's a static resource, just continue the filter chain and skip the logic below @@ -36,11 +36,14 @@ public class FirstLoginFilter extends OncePerRequestFilter { filterChain.doFilter(request, response); return; } - + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.isAuthenticated()) { Optional user = userService.findByUsername(authentication.getName()); - if ("GET".equalsIgnoreCase(method) && user.isPresent() && user.get().isFirstLogin() && !"/change-creds".equals(requestURI)) { + if ("GET".equalsIgnoreCase(method) + && user.isPresent() + && user.get().isFirstLogin() + && !"/change-creds".equals(requestURI)) { response.sendRedirect("/change-creds"); return; } diff --git a/src/main/java/stirling/software/SPDF/config/security/IPRateLimitingFilter.java b/src/main/java/stirling/software/SPDF/config/security/IPRateLimitingFilter.java index 03e34b57..b79cd586 100644 --- a/src/main/java/stirling/software/SPDF/config/security/IPRateLimitingFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/IPRateLimitingFilter.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.config.security; + import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; @@ -13,51 +14,53 @@ import stirling.software.SPDF.utils.RequestUriUtils; public class IPRateLimitingFilter implements Filter { - private final ConcurrentHashMap requestCounts = new ConcurrentHashMap<>(); + private final ConcurrentHashMap requestCounts = + new ConcurrentHashMap<>(); private final ConcurrentHashMap getCounts = new ConcurrentHashMap<>(); private final int maxRequests; private final int maxGetRequests; - + public IPRateLimitingFilter(int maxRequests, int maxGetRequests) { this.maxRequests = maxRequests; this.maxGetRequests = maxGetRequests; } @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - if (request instanceof HttpServletRequest) { - HttpServletRequest httpRequest = (HttpServletRequest) request; - String method = httpRequest.getMethod(); - String requestURI = httpRequest.getRequestURI(); - // Check if the request is for static resources - boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI); + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + if (request instanceof HttpServletRequest) { + HttpServletRequest httpRequest = (HttpServletRequest) request; + String method = httpRequest.getMethod(); + String requestURI = httpRequest.getRequestURI(); + // Check if the request is for static resources + boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI); - // If it's a static resource, just continue the filter chain and skip the logic below - if (isStaticResource) { - chain.doFilter(request, response); - return; - } - - String clientIp = request.getRemoteAddr(); - requestCounts.computeIfAbsent(clientIp, k -> new AtomicInteger(0)); - if (!"GET".equalsIgnoreCase(method)) { - - if (requestCounts.get(clientIp).incrementAndGet() > maxRequests) { - // Handle limit exceeded (e.g., send error response) - response.getWriter().write("Rate limit exceeded"); - return; - } - } else { - if (requestCounts.get(clientIp).incrementAndGet() > maxGetRequests) { - // Handle limit exceeded (e.g., send error response) - response.getWriter().write("GET Rate limit exceeded"); - return; - } - } - } - chain.doFilter(request, response); + // If it's a static resource, just continue the filter chain and skip the logic below + if (isStaticResource) { + chain.doFilter(request, response); + return; + } + + String clientIp = request.getRemoteAddr(); + requestCounts.computeIfAbsent(clientIp, k -> new AtomicInteger(0)); + if (!"GET".equalsIgnoreCase(method)) { + + if (requestCounts.get(clientIp).incrementAndGet() > maxRequests) { + // Handle limit exceeded (e.g., send error response) + response.getWriter().write("Rate limit exceeded"); + return; + } + } else { + if (requestCounts.get(clientIp).incrementAndGet() > maxGetRequests) { + // Handle limit exceeded (e.g., send error response) + response.getWriter().write("GET Rate limit exceeded"); + return; + } + } + } + chain.doFilter(request, response); } - + public void resetRequestCounts() { requestCounts.clear(); getCounts.clear(); diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java index 5d100dd8..3b396b15 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -13,75 +13,76 @@ import org.springframework.stereotype.Component; import jakarta.annotation.PostConstruct; import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.Role; + @Component public class InitialSecuritySetup { - @Autowired - private UserService userService; + @Autowired private UserService userService; + @Autowired ApplicationProperties applicationProperties; - @Autowired - ApplicationProperties applicationProperties; - - @PostConstruct - public void init() { - if (!userService.hasUsers()) { - - - String initialUsername = applicationProperties.getSecurity().getInitialLogin().getUsername(); - String initialPassword = applicationProperties.getSecurity().getInitialLogin().getPassword(); - if (initialUsername != null && initialPassword != null) { - userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); - } else { - initialUsername = "admin"; - initialPassword = "stirling"; - userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId(), true); - } - } - if(!userService.usernameExists(Role.INTERNAL_API_USER.getRoleId())) { - userService.saveUser(Role.INTERNAL_API_USER.getRoleId(), UUID.randomUUID().toString(), Role.INTERNAL_API_USER.getRoleId()); - userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId()); - } - } + @PostConstruct + public void init() { + if (!userService.hasUsers()) { + String initialUsername = + applicationProperties.getSecurity().getInitialLogin().getUsername(); + String initialPassword = + applicationProperties.getSecurity().getInitialLogin().getPassword(); + if (initialUsername != null && initialPassword != null) { + userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); + } else { + initialUsername = "admin"; + initialPassword = "stirling"; + userService.saveUser( + initialUsername, initialPassword, Role.ADMIN.getRoleId(), true); + } + } + if (!userService.usernameExists(Role.INTERNAL_API_USER.getRoleId())) { + userService.saveUser( + Role.INTERNAL_API_USER.getRoleId(), + UUID.randomUUID().toString(), + Role.INTERNAL_API_USER.getRoleId()); + userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId()); + } + } + @PostConstruct + public void initSecretKey() throws IOException { + String secretKey = applicationProperties.getAutomaticallyGenerated().getKey(); + if (secretKey == null || secretKey.isEmpty()) { + secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key + saveKeyToConfig(secretKey); + } + } - @PostConstruct - public void initSecretKey() throws IOException { - String secretKey = applicationProperties.getAutomaticallyGenerated().getKey(); - if (secretKey == null || secretKey.isEmpty()) { - secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key - saveKeyToConfig(secretKey); - } - } + private void saveKeyToConfig(String key) throws IOException { + Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml + List lines = Files.readAllLines(path); + boolean keyFound = false; - private void saveKeyToConfig(String key) throws IOException { - Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml - List lines = Files.readAllLines(path); - boolean keyFound = false; + // Search for the existing key to replace it or place to add it + for (int i = 0; i < lines.size(); i++) { + if (lines.get(i).startsWith("AutomaticallyGenerated:")) { + keyFound = true; + if (i + 1 < lines.size() && lines.get(i + 1).trim().startsWith("key:")) { + lines.set(i + 1, " key: " + key); + break; + } else { + lines.add(i + 1, " key: " + key); + break; + } + } + } - // Search for the existing key to replace it or place to add it - for (int i = 0; i < lines.size(); i++) { - if (lines.get(i).startsWith("AutomaticallyGenerated:")) { - keyFound = true; - if (i + 1 < lines.size() && lines.get(i + 1).trim().startsWith("key:")) { - lines.set(i + 1, " key: " + key); - break; - } else { - lines.add(i + 1, " key: " + key); - break; - } - } - } + // If the section doesn't exist, append it + if (!keyFound) { + lines.add("# Automatically Generated Settings (Do Not Edit Directly)"); + lines.add("AutomaticallyGenerated:"); + lines.add(" key: " + key); + } - // If the section doesn't exist, append it - if (!keyFound) { - lines.add("# Automatically Generated Settings (Do Not Edit Directly)"); - lines.add("AutomaticallyGenerated:"); - lines.add(" key: " + key); - } - - // Write back to the file - Files.write(path, lines); - } -} \ No newline at end of file + // Write back to the file + Files.write(path, lines); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/LoginAttemptService.java b/src/main/java/stirling/software/SPDF/config/security/LoginAttemptService.java index 55a84449..40a54ecc 100644 --- a/src/main/java/stirling/software/SPDF/config/security/LoginAttemptService.java +++ b/src/main/java/stirling/software/SPDF/config/security/LoginAttemptService.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.config.security; + import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @@ -12,39 +13,41 @@ import stirling.software.SPDF.model.AttemptCounter; @Service public class LoginAttemptService { - - @Autowired - ApplicationProperties applicationProperties; - + @Autowired ApplicationProperties applicationProperties; + private int MAX_ATTEMPTS; private long ATTEMPT_INCREMENT_TIME; - - + @PostConstruct public void init() { - MAX_ATTEMPTS = applicationProperties.getSecurity().getLoginAttemptCount(); - ATTEMPT_INCREMENT_TIME = TimeUnit.MINUTES.toMillis(applicationProperties.getSecurity().getLoginResetTimeMinutes()); + MAX_ATTEMPTS = applicationProperties.getSecurity().getLoginAttemptCount(); + ATTEMPT_INCREMENT_TIME = + TimeUnit.MINUTES.toMillis( + applicationProperties.getSecurity().getLoginResetTimeMinutes()); } - - private final ConcurrentHashMap attemptsCache = new ConcurrentHashMap<>(); + + private final ConcurrentHashMap attemptsCache = + new ConcurrentHashMap<>(); public void loginSucceeded(String key) { attemptsCache.remove(key); } public boolean loginAttemptCheck(String key) { - attemptsCache.compute(key, (k, attemptCounter) -> { - if (attemptCounter == null || attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) { - return new AttemptCounter(); - } else { - attemptCounter.increment(); - return attemptCounter; - } - }); + attemptsCache.compute( + key, + (k, attemptCounter) -> { + if (attemptCounter == null + || attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) { + return new AttemptCounter(); + } else { + attemptCounter.increment(); + return attemptCounter; + } + }); return attemptsCache.get(key).getAttemptCount() >= MAX_ATTEMPTS; } - public boolean isBlocked(String key) { AttemptCounter attemptCounter = attemptsCache.get(key); if (attemptCounter != null) { @@ -52,5 +55,4 @@ public class LoginAttemptService { } return false; } - } diff --git a/src/main/java/stirling/software/SPDF/config/security/RateLimitResetScheduler.java b/src/main/java/stirling/software/SPDF/config/security/RateLimitResetScheduler.java index 3ef8ef31..a3641a70 100644 --- a/src/main/java/stirling/software/SPDF/config/security/RateLimitResetScheduler.java +++ b/src/main/java/stirling/software/SPDF/config/security/RateLimitResetScheduler.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.config.security; + import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -11,7 +12,7 @@ public class RateLimitResetScheduler { this.rateLimitingFilter = rateLimitingFilter; } - @Scheduled(cron = "0 0 0 * * MON") // At 00:00 every Monday TODO: configurable + @Scheduled(cron = "0 0 0 * * MON") // At 00:00 every Monday TODO: configurable public void resetRateLimit() { rateLimitingFilter.resetRequestCounts(); } diff --git a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java index e0b439db..8fd0b401 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -19,104 +19,108 @@ import org.springframework.security.web.savedrequest.NullRequestCache; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import stirling.software.SPDF.repository.JPATokenRepositoryImpl; + @Configuration @EnableWebSecurity() @EnableMethodSecurity public class SecurityConfiguration { - @Autowired - private UserDetailsService userDetailsService; + @Autowired private UserDetailsService userDetailsService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - @Autowired - @Lazy - private UserService userService; - - @Autowired - @Qualifier("loginEnabled") - public boolean loginEnabledValue; - - @Autowired - private UserAuthenticationFilter userAuthenticationFilter; - + @Autowired @Lazy private UserService userService; @Autowired - private LoginAttemptService loginAttemptService; - - @Autowired - private FirstLoginFilter firstLoginFilter; - + @Qualifier("loginEnabled") public boolean loginEnabledValue; + + @Autowired private UserAuthenticationFilter userAuthenticationFilter; + + @Autowired private LoginAttemptService loginAttemptService; + + @Autowired private FirstLoginFilter firstLoginFilter; + @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); - - if(loginEnabledValue) { - - http.csrf(csrf -> csrf.disable()); - http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class); - http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class); - http - .formLogin(formLogin -> formLogin - .loginPage("/login") - .successHandler(new CustomAuthenticationSuccessHandler()) - .defaultSuccessUrl("/") - .failureHandler(new CustomAuthenticationFailureHandler(loginAttemptService)) - .permitAll() - ).requestCache(requestCache -> requestCache - .requestCache(new NullRequestCache()) - ) - .logout(logout -> logout - .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) - .logoutSuccessUrl("/login?logout=true") - .invalidateHttpSession(true) // Invalidate session - .deleteCookies("JSESSIONID", "remember-me") - ).rememberMe(rememberMeConfigurer -> rememberMeConfigurer // Use the configurator directly - .key("uniqueAndSecret") - .tokenRepository(persistentTokenRepository()) - .tokenValiditySeconds(1209600) // 2 weeks - ) - .authorizeHttpRequests(authz -> authz - .requestMatchers(req -> { - String uri = req.getRequestURI(); - String contextPath = req.getContextPath(); + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); - // Remove the context path from the URI - String trimmedUri = uri.startsWith(contextPath) ? uri.substring(contextPath.length()) : uri; + if (loginEnabledValue) { - return trimmedUri.startsWith("/login") || trimmedUri.endsWith(".svg") || - trimmedUri.startsWith("/register") || trimmedUri.startsWith("/error") || - trimmedUri.startsWith("/images/") || trimmedUri.startsWith("/public/") || - trimmedUri.startsWith("/css/") || trimmedUri.startsWith("/js/"); - } - ).permitAll() - .anyRequest().authenticated() - ) - .userDetailsService(userDetailsService) - .authenticationProvider(authenticationProvider()); - } else { - http.csrf(csrf -> csrf.disable()) - .authorizeHttpRequests(authz -> authz - .anyRequest().permitAll() - ); - } + http.csrf(csrf -> csrf.disable()); + http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class); + http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class); + http.formLogin( + formLogin -> + formLogin + .loginPage("/login") + .successHandler( + new CustomAuthenticationSuccessHandler()) + .defaultSuccessUrl("/") + .failureHandler( + new CustomAuthenticationFailureHandler( + loginAttemptService)) + .permitAll()) + .requestCache(requestCache -> requestCache.requestCache(new NullRequestCache())) + .logout( + logout -> + logout.logoutRequestMatcher( + new AntPathRequestMatcher("/logout")) + .logoutSuccessUrl("/login?logout=true") + .invalidateHttpSession(true) // Invalidate session + .deleteCookies("JSESSIONID", "remember-me")) + .rememberMe( + rememberMeConfigurer -> + rememberMeConfigurer // Use the configurator directly + .key("uniqueAndSecret") + .tokenRepository(persistentTokenRepository()) + .tokenValiditySeconds(1209600) // 2 weeks + ) + .authorizeHttpRequests( + authz -> + authz.requestMatchers( + req -> { + String uri = req.getRequestURI(); + String contextPath = req.getContextPath(); + + // Remove the context path from the URI + String trimmedUri = + uri.startsWith(contextPath) + ? uri.substring( + contextPath + .length()) + : uri; + + return trimmedUri.startsWith("/login") + || trimmedUri.endsWith(".svg") + || trimmedUri.startsWith( + "/register") + || trimmedUri.startsWith("/error") + || trimmedUri.startsWith("/images/") + || trimmedUri.startsWith("/public/") + || trimmedUri.startsWith("/css/") + || trimmedUri.startsWith("/js/"); + }) + .permitAll() + .anyRequest() + .authenticated()) + .userDetailsService(userDetailsService) + .authenticationProvider(authenticationProvider()); + } else { + http.csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(authz -> authz.anyRequest().permitAll()); + } return http.build(); } - - - @Bean public IPRateLimitingFilter rateLimitingFilter() { int maxRequestsPerIp = 1000000; // Example limit TODO add config level return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp); } - - @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); @@ -124,13 +128,9 @@ public class SecurityConfiguration { authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; } - + @Bean public PersistentTokenRepository persistentTokenRepository() { return new JPATokenRepositoryImpl(); } - - - } - diff --git a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java index ce77e5a4..6b8d047c 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java @@ -19,32 +19,28 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import stirling.software.SPDF.model.ApiKeyAuthenticationToken; + @Component public class UserAuthenticationFilter extends OncePerRequestFilter { - @Autowired - private UserDetailsService userDetailsService; + @Autowired private UserDetailsService userDetailsService; + + @Autowired @Lazy private UserService userService; @Autowired - @Lazy - private UserService userService; - - - @Autowired - @Qualifier("loginEnabled") - public boolean loginEnabledValue; + @Qualifier("loginEnabled") public boolean loginEnabledValue; @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { if (!loginEnabledValue) { // If login is not enabled, just pass all requests without authentication filterChain.doFilter(request, response); return; } - String requestURI = request.getRequestURI(); + String requestURI = request.getRequestURI(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // Check for API key in the request headers if no authentication exists @@ -52,15 +48,17 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { String apiKey = request.getHeader("X-API-Key"); if (apiKey != null && !apiKey.trim().isEmpty()) { try { - // Use API key to authenticate. This requires you to have an authentication provider for API keys. - UserDetails userDetails = userService.loadUserByApiKey(apiKey); - if(userDetails == null) - { - response.setStatus(HttpStatus.UNAUTHORIZED.value()); + // Use API key to authenticate. This requires you to have an authentication + // provider for API keys. + UserDetails userDetails = userService.loadUserByApiKey(apiKey); + if (userDetails == null) { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write("Invalid API Key."); return; - } - authentication = new ApiKeyAuthenticationToken(userDetails, apiKey, userDetails.getAuthorities()); + } + authentication = + new ApiKeyAuthenticationToken( + userDetails, apiKey, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (AuthenticationException e) { // If API key authentication fails, deny the request @@ -73,36 +71,38 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { // If we still don't have any authentication, deny the request if (authentication == null || !authentication.isAuthenticated()) { - String method = request.getMethod(); - String contextPath = request.getContextPath(); - - if ("GET".equalsIgnoreCase(method) && ! (contextPath + "/login").equals(requestURI)) { - response.sendRedirect(contextPath + "/login"); // redirect to the login page - return; + String method = request.getMethod(); + String contextPath = request.getContextPath(); + + if ("GET".equalsIgnoreCase(method) && !(contextPath + "/login").equals(requestURI)) { + response.sendRedirect(contextPath + "/login"); // redirect to the login page + return; } else { - response.setStatus(HttpStatus.UNAUTHORIZED.value()); - response.getWriter().write("Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected"); - return; + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.getWriter() + .write( + "Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected"); + return; } - } + } filterChain.doFilter(request, response); } - + @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { String uri = request.getRequestURI(); String contextPath = request.getContextPath(); String[] permitAllPatterns = { - contextPath + "/login", - contextPath + "/register", - contextPath + "/error", - contextPath + "/images/", - contextPath + "/public/", - contextPath + "/css/", - contextPath + "/js/", - contextPath + "/pdfjs/", - contextPath + "/site.webmanifest" + contextPath + "/login", + contextPath + "/register", + contextPath + "/error", + contextPath + "/images/", + contextPath + "/public/", + contextPath + "/css/", + contextPath + "/js/", + contextPath + "/pdfjs/", + contextPath + "/site.webmanifest" }; for (String pattern : permitAllPatterns) { @@ -113,5 +113,4 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { return false; } - } diff --git a/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java index f23e5ce3..cadbfb24 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java @@ -20,28 +20,28 @@ import io.github.bucket4j.Bandwidth; import io.github.bucket4j.Bucket; import io.github.bucket4j.ConsumptionProbe; import io.github.bucket4j.Refill; + import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import stirling.software.SPDF.model.Role; + @Component public class UserBasedRateLimitingFilter extends OncePerRequestFilter { - private final Map apiBuckets = new ConcurrentHashMap<>(); + private final Map apiBuckets = new ConcurrentHashMap<>(); private final Map webBuckets = new ConcurrentHashMap<>(); - @Autowired - private UserDetailsService userDetailsService; + @Autowired private UserDetailsService userDetailsService; @Autowired - @Qualifier("rateLimit") - public boolean rateLimit; + @Qualifier("rateLimit") public boolean rateLimit; @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { if (!rateLimit) { // If rateLimit is not enabled, just pass all requests without rate limiting filterChain.doFilter(request, response); @@ -60,7 +60,8 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { // Check for API key in the request headers String apiKey = request.getHeader("X-API-Key"); if (apiKey != null && !apiKey.trim().isEmpty()) { - identifier = "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames + identifier = + "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames } else { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.isAuthenticated()) { @@ -74,14 +75,27 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { identifier = request.getRemoteAddr(); } - Role userRole = getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication()); + Role userRole = + getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication()); if (request.getHeader("X-API-Key") != null) { // It's an API call - processRequest(userRole.getApiCallsPerDay(), identifier, apiBuckets, request, response, filterChain); + processRequest( + userRole.getApiCallsPerDay(), + identifier, + apiBuckets, + request, + response, + filterChain); } else { // It's a Web UI call - processRequest(userRole.getWebCallsPerDay(), identifier, webBuckets, request, response, filterChain); + processRequest( + userRole.getWebCallsPerDay(), + identifier, + webBuckets, + request, + response, + filterChain); } } @@ -98,8 +112,13 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { throw new IllegalStateException("User does not have a valid role."); } - private void processRequest(int limitPerDay, String identifier, Map buckets, - HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + private void processRequest( + int limitPerDay, + String identifier, + Map buckets, + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws IOException, ServletException { Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay)); ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1); @@ -116,10 +135,8 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { } private Bucket createUserBucket(int limitPerDay) { - Bandwidth limit = Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1))); + Bandwidth limit = + Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1))); return Bucket.builder().addLimit(limit).build(); } } - - - diff --git a/src/main/java/stirling/software/SPDF/config/security/UserService.java b/src/main/java/stirling/software/SPDF/config/security/UserService.java index 45794d92..986bb16f 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserService.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.config.security; + import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -21,38 +22,35 @@ import stirling.software.SPDF.model.Authority; import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.User; import stirling.software.SPDF.repository.UserRepository; -@Service -public class UserService implements UserServiceInterface{ - - @Autowired - private UserRepository userRepository; - @Autowired - private PasswordEncoder passwordEncoder; +@Service +public class UserService implements UserServiceInterface { + + @Autowired private UserRepository userRepository; + + @Autowired private PasswordEncoder passwordEncoder; public Authentication getAuthentication(String apiKey) { User user = getUserByApiKey(apiKey); if (user == null) { throw new UsernameNotFoundException("API key is not valid"); } - + // Convert the user into an Authentication object return new UsernamePasswordAuthenticationToken( - user, // principal (typically the user) - null, // credentials (we don't expose the password or API key here) - getAuthorities(user) // user's authorities (roles/permissions) - ); + user, // principal (typically the user) + null, // credentials (we don't expose the password or API key here) + getAuthorities(user) // user's authorities (roles/permissions) + ); } - + private Collection getAuthorities(User user) { // Convert each Authority object into a SimpleGrantedAuthority object. - return user.getAuthorities().stream() - .map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority())) - .collect(Collectors.toList()); - - + return user.getAuthorities().stream() + .map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority())) + .collect(Collectors.toList()); } - + private String generateApiKey() { String apiKey; do { @@ -62,9 +60,11 @@ public class UserService implements UserServiceInterface{ } public User addApiKeyToUser(String username) { - User user = userRepository.findByUsername(username) - .orElseThrow(() -> new UsernameNotFoundException("User not found")); - + User user = + userRepository + .findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + user.setApiKey(generateApiKey()); return userRepository.save(user); } @@ -74,8 +74,10 @@ public class UserService implements UserServiceInterface{ } public String getApiKeyForUser(String username) { - User user = userRepository.findByUsername(username) - .orElseThrow(() -> new UsernameNotFoundException("User not found")); + User user = + userRepository + .findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); return user.getApiKey(); } @@ -86,27 +88,25 @@ public class UserService implements UserServiceInterface{ public User getUserByApiKey(String apiKey) { return userRepository.findByApiKey(apiKey); } - + public UserDetails loadUserByApiKey(String apiKey) { User userOptional = userRepository.findByApiKey(apiKey); if (userOptional != null) { User user = userOptional; // Convert your User entity to a UserDetails object with authorities return new org.springframework.security.core.userdetails.User( - user.getUsername(), - user.getPassword(), // you might not need this for API key auth - getAuthorities(user) - ); + user.getUsername(), + user.getPassword(), // you might not need this for API key auth + getAuthorities(user)); } - return null; // or throw an exception + return null; // or throw an exception } - public boolean validateApiKeyForUser(String username, String apiKey) { Optional userOpt = userRepository.findByUsername(username); return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey); } - + public void saveUser(String username, String password) { User user = new User(); user.setUsername(username); @@ -124,7 +124,7 @@ public class UserService implements UserServiceInterface{ user.setFirstLogin(firstLogin); userRepository.save(user); } - + public void saveUser(String username, String password, String role) { User user = new User(); user.setUsername(username); @@ -134,42 +134,42 @@ public class UserService implements UserServiceInterface{ user.setFirstLogin(false); userRepository.save(user); } - + public void deleteUser(String username) { - Optional userOpt = userRepository.findByUsername(username); - if (userOpt.isPresent()) { - for (Authority authority : userOpt.get().getAuthorities()) { - if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) { - return; - } - } - userRepository.delete(userOpt.get()); - } + Optional userOpt = userRepository.findByUsername(username); + if (userOpt.isPresent()) { + for (Authority authority : userOpt.get().getAuthorities()) { + if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) { + return; + } + } + userRepository.delete(userOpt.get()); + } } - + public boolean usernameExists(String username) { return userRepository.findByUsername(username).isPresent(); } - + public boolean hasUsers() { return userRepository.count() > 0; } - + public void updateUserSettings(String username, Map updates) { Optional userOpt = userRepository.findByUsername(username); if (userOpt.isPresent()) { User user = userOpt.get(); Map settingsMap = user.getSettings(); - if(settingsMap == null) { - settingsMap = new HashMap(); - } + if (settingsMap == null) { + settingsMap = new HashMap(); + } settingsMap.clear(); settingsMap.putAll(updates); user.setSettings(settingsMap); userRepository.save(user); - } + } } public Optional findByUsername(String username) { @@ -185,13 +185,12 @@ public class UserService implements UserServiceInterface{ user.setPassword(passwordEncoder.encode(newPassword)); userRepository.save(user); } - + public void changeFirstUse(User user, boolean firstUse) { user.setFirstLogin(firstUse); userRepository.save(user); } - - + public boolean isPasswordCorrect(User user, String currentPassword) { return passwordEncoder.matches(currentPassword, user.getPassword()); } diff --git a/src/main/java/stirling/software/SPDF/controller/api/CropController.java b/src/main/java/stirling/software/SPDF/controller/api/CropController.java index d2723cce..a547d89f 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/CropController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/CropController.java @@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.general.CropPdfForm; import stirling.software.SPDF.utils.WebResponseUtils; @@ -28,59 +29,62 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "General", description = "General APIs") public class CropController { - private static final Logger logger = LoggerFactory.getLogger(CropController.class); + private static final Logger logger = LoggerFactory.getLogger(CropController.class); - @PostMapping(value = "/crop", consumes = "multipart/form-data") - @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(@ModelAttribute CropPdfForm form) - throws IOException { + @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(@ModelAttribute CropPdfForm form) throws IOException { + PDDocument sourceDocument = + PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes())); - + PDDocument newDocument = new PDDocument(); -PDDocument sourceDocument = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes())); + int totalPages = sourceDocument.getNumberOfPages(); -PDDocument newDocument = new PDDocument(); + LayerUtility layerUtility = new LayerUtility(newDocument); -int totalPages = sourceDocument.getNumberOfPages(); + for (int i = 0; i < totalPages; i++) { + PDPage sourcePage = sourceDocument.getPage(i); -LayerUtility layerUtility = new LayerUtility(newDocument); + // Create a new page with the size of the source page + PDPage newPage = new PDPage(sourcePage.getMediaBox()); + newDocument.addPage(newPage); + PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage); -for (int i = 0; i < totalPages; i++) { - PDPage sourcePage = sourceDocument.getPage(i); - - // Create a new page with the size of the source page - PDPage newPage = new PDPage(sourcePage.getMediaBox()); - newDocument.addPage(newPage); - PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage); + // Import the source page as a form XObject + PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i); - // Import the source page as a form XObject - PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i); + contentStream.saveGraphicsState(); - contentStream.saveGraphicsState(); - - // Define the crop area - contentStream.addRect(form.getX(), form.getY(), form.getWidth(), form.getHeight()); - contentStream.clip(); + // Define the crop area + contentStream.addRect(form.getX(), form.getY(), form.getWidth(), form.getHeight()); + contentStream.clip(); - // Draw the entire formXObject - contentStream.drawForm(formXObject); + // Draw the entire formXObject + contentStream.drawForm(formXObject); - contentStream.restoreGraphicsState(); - - contentStream.close(); - - // Now, set the new page's media box to the cropped size - newPage.setMediaBox(new PDRectangle(form.getX(), form.getY(), form.getWidth(), form.getHeight())); -} - -ByteArrayOutputStream baos = new ByteArrayOutputStream(); -newDocument.save(baos); -newDocument.close(); -sourceDocument.close(); - -byte[] pdfContent = baos.toByteArray(); -return WebResponseUtils.bytesToWebResponse(pdfContent, form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf"); - } + contentStream.restoreGraphicsState(); + contentStream.close(); + + // Now, set the new page's media box to the cropped size + newPage.setMediaBox( + new PDRectangle(form.getX(), form.getY(), form.getWidth(), form.getHeight())); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + newDocument.save(baos); + newDocument.close(); + sourceDocument.close(); + + byte[] pdfContent = baos.toByteArray(); + return WebResponseUtils.bytesToWebResponse( + pdfContent, + form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_cropped.pdf"); + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/MergeController.java b/src/main/java/stirling/software/SPDF/controller/api/MergeController.java index 7db00a31..70b79191 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/MergeController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/MergeController.java @@ -1,7 +1,15 @@ package stirling.software.SPDF.controller.api; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + import org.apache.pdfbox.io.MemoryUsageSetting; import org.apache.pdfbox.multipdf.PDFMergerUtility; import org.apache.pdfbox.pdmodel.PDDocument; @@ -14,19 +22,13 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.general.MergePdfsRequest; import stirling.software.SPDF.utils.WebResponseUtils; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; - @RestController @RequestMapping("/api/v1/general") @Tag(name = "General", description = "General APIs") @@ -34,7 +36,6 @@ public class MergeController { private static final Logger logger = LoggerFactory.getLogger(MergeController.class); - private PDDocument mergeDocuments(List documents) throws IOException { PDDocument mergedDoc = new PDDocument(); for (PDDocument doc : documents) { @@ -52,27 +53,39 @@ public class MergeController { case "byDateModified": return (file1, file2) -> { try { - BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class); - BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class); + BasicFileAttributes attr1 = + Files.readAttributes( + Paths.get(file1.getOriginalFilename()), + BasicFileAttributes.class); + BasicFileAttributes attr2 = + Files.readAttributes( + Paths.get(file2.getOriginalFilename()), + BasicFileAttributes.class); return attr1.lastModifiedTime().compareTo(attr2.lastModifiedTime()); } catch (IOException e) { - return 0; // If there's an error, treat them as equal + return 0; // If there's an error, treat them as equal } }; case "byDateCreated": return (file1, file2) -> { try { - BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class); - BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class); + BasicFileAttributes attr1 = + Files.readAttributes( + Paths.get(file1.getOriginalFilename()), + BasicFileAttributes.class); + BasicFileAttributes attr2 = + Files.readAttributes( + Paths.get(file2.getOriginalFilename()), + BasicFileAttributes.class); return attr1.creationTime().compareTo(attr2.creationTime()); } catch (IOException e) { - return 0; // If there's an error, treat them as equal + return 0; // If there's an error, treat them as equal } }; case "byPDFTitle": return (file1, file2) -> { try (PDDocument doc1 = PDDocument.load(file1.getInputStream()); - PDDocument doc2 = PDDocument.load(file2.getInputStream())) { + PDDocument doc2 = PDDocument.load(file2.getInputStream())) { String title1 = doc1.getDocumentInformation().getTitle(); String title2 = doc2.getDocumentInformation().getTitle(); return title1.compareTo(title2); @@ -82,14 +95,17 @@ public class MergeController { }; case "orderProvided": default: - return (file1, file2) -> 0; // Default is the order provided + return (file1, file2) -> 0; // Default is the order provided } } @PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs") - @Operation(summary = "Merge multiple PDF files into one", - description = "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO") - public ResponseEntity mergePdfs(@ModelAttribute MergePdfsRequest form) throws IOException { + @Operation( + summary = "Merge multiple PDF files into one", + description = + "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO") + public ResponseEntity mergePdfs(@ModelAttribute MergePdfsRequest form) + throws IOException { try { MultipartFile[] files = form.getFileInput(); Arrays.sort(files, getSortComparator(form.getSortType())); @@ -101,14 +117,16 @@ public class MergeController { mergedDoc.addSource(new ByteArrayInputStream(file.getBytes())); } - mergedDoc.setDestinationFileName(files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf"); + mergedDoc.setDestinationFileName( + files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf"); mergedDoc.setDestinationStream(docOutputstream); mergedDoc.mergeDocuments(MemoryUsageSetting.setupMainMemoryOnly()); - return WebResponseUtils.bytesToWebResponse(docOutputstream.toByteArray(), mergedDoc.getDestinationFileName()); + return WebResponseUtils.bytesToWebResponse( + docOutputstream.toByteArray(), mergedDoc.getDestinationFileName()); } catch (Exception ex) { logger.error("Error in merge pdf process", ex); - throw ex; + throw ex; } } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java b/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java index ebe81ffd..52127571 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api; - import java.awt.Color; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -23,6 +22,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest; import stirling.software.SPDF.utils.WebResponseUtils; @@ -31,94 +31,110 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "General", description = "General APIs") public class MultiPageLayoutController { - private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class); + private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class); - @PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data") - @Operation( - summary = "Merge multiple pages of a PDF document into a single page", - description = "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO" - ) - public ResponseEntity mergeMultiplePagesIntoOne(@ModelAttribute MergeMultiplePagesRequest request) - throws IOException { + @PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data") + @Operation( + summary = "Merge multiple pages of a PDF document into a single page", + description = + "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO") + public ResponseEntity mergeMultiplePagesIntoOne( + @ModelAttribute MergeMultiplePagesRequest request) throws IOException { - int pagesPerSheet = request.getPagesPerSheet(); - MultipartFile file = request.getFileInput(); - boolean addBorder = request.isAddBorder(); - - if (pagesPerSheet != 2 && pagesPerSheet != 3 && pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) { - throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square"); - } + int pagesPerSheet = request.getPagesPerSheet(); + MultipartFile file = request.getFileInput(); + boolean addBorder = request.isAddBorder(); - int cols = pagesPerSheet == 2 || pagesPerSheet == 3 ? pagesPerSheet : (int) Math.sqrt(pagesPerSheet); - int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet); + if (pagesPerSheet != 2 + && pagesPerSheet != 3 + && pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) { + throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square"); + } - PDDocument sourceDocument = PDDocument.load(file.getInputStream()); - PDDocument newDocument = new PDDocument(); - PDPage newPage = new PDPage(PDRectangle.A4); - newDocument.addPage(newPage); + int cols = + pagesPerSheet == 2 || pagesPerSheet == 3 + ? pagesPerSheet + : (int) Math.sqrt(pagesPerSheet); + int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet); - int totalPages = sourceDocument.getNumberOfPages(); - float cellWidth = newPage.getMediaBox().getWidth() / cols; - float cellHeight = newPage.getMediaBox().getHeight() / rows; + PDDocument sourceDocument = PDDocument.load(file.getInputStream()); + PDDocument newDocument = new PDDocument(); + PDPage newPage = new PDPage(PDRectangle.A4); + newDocument.addPage(newPage); - PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true); - LayerUtility layerUtility = new LayerUtility(newDocument); + int totalPages = sourceDocument.getNumberOfPages(); + float cellWidth = newPage.getMediaBox().getWidth() / cols; + float cellHeight = newPage.getMediaBox().getHeight() / rows; - float borderThickness = 1.5f; // Specify border thickness as required - contentStream.setLineWidth(borderThickness); - contentStream.setStrokingColor(Color.BLACK); - - for (int i = 0; i < totalPages; i++) { - if (i != 0 && i % pagesPerSheet == 0) { - // Close the current content stream and create a new page and content stream - contentStream.close(); - newPage = new PDPage(PDRectangle.A4); - newDocument.addPage(newPage); - contentStream = new PDPageContentStream(newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true); - } + PDPageContentStream contentStream = + new PDPageContentStream( + newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true); + LayerUtility layerUtility = new LayerUtility(newDocument); - PDPage sourcePage = sourceDocument.getPage(i); - PDRectangle rect = sourcePage.getMediaBox(); - float scaleWidth = cellWidth / rect.getWidth(); - float scaleHeight = cellHeight / rect.getHeight(); - float scale = Math.min(scaleWidth, scaleHeight); + float borderThickness = 1.5f; // Specify border thickness as required + contentStream.setLineWidth(borderThickness); + contentStream.setStrokingColor(Color.BLACK); - int adjustedPageIndex = i % pagesPerSheet; // This will reset the index for every new page - int rowIndex = adjustedPageIndex / cols; - int colIndex = adjustedPageIndex % cols; + for (int i = 0; i < totalPages; i++) { + if (i != 0 && i % pagesPerSheet == 0) { + // Close the current content stream and create a new page and content stream + contentStream.close(); + newPage = new PDPage(PDRectangle.A4); + newDocument.addPage(newPage); + contentStream = + new PDPageContentStream( + newDocument, + newPage, + PDPageContentStream.AppendMode.APPEND, + true, + true); + } - float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2; - float y = newPage.getMediaBox().getHeight() - ((rowIndex + 1) * cellHeight - (cellHeight - rect.getHeight() * scale) / 2); + PDPage sourcePage = sourceDocument.getPage(i); + PDRectangle rect = sourcePage.getMediaBox(); + float scaleWidth = cellWidth / rect.getWidth(); + float scaleHeight = cellHeight / rect.getHeight(); + float scale = Math.min(scaleWidth, scaleHeight); - contentStream.saveGraphicsState(); - contentStream.transform(Matrix.getTranslateInstance(x, y)); - contentStream.transform(Matrix.getScaleInstance(scale, scale)); + int adjustedPageIndex = + i % pagesPerSheet; // This will reset the index for every new page + int rowIndex = adjustedPageIndex / cols; + int colIndex = adjustedPageIndex % cols; - PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i); - contentStream.drawForm(formXObject); + float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2; + float y = + newPage.getMediaBox().getHeight() + - ((rowIndex + 1) * cellHeight + - (cellHeight - rect.getHeight() * scale) / 2); - contentStream.restoreGraphicsState(); - - if(addBorder) { - // Draw border around each page - float borderX = colIndex * cellWidth; - float borderY = newPage.getMediaBox().getHeight() - (rowIndex + 1) * cellHeight; - contentStream.addRect(borderX, borderY, cellWidth, cellHeight); - contentStream.stroke(); - } - } + contentStream.saveGraphicsState(); + contentStream.transform(Matrix.getTranslateInstance(x, y)); + contentStream.transform(Matrix.getScaleInstance(scale, scale)); + PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i); + contentStream.drawForm(formXObject); - contentStream.close(); // Close the final content stream - sourceDocument.close(); + contentStream.restoreGraphicsState(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - newDocument.save(baos); - newDocument.close(); + if (addBorder) { + // Draw border around each page + float borderX = colIndex * cellWidth; + float borderY = newPage.getMediaBox().getHeight() - (rowIndex + 1) * cellHeight; + contentStream.addRect(borderX, borderY, cellWidth, cellHeight); + contentStream.stroke(); + } + } - byte[] result = baos.toByteArray(); - return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf"); - } + contentStream.close(); // Close the final content stream + sourceDocument.close(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + newDocument.save(baos); + newDocument.close(); + byte[] result = baos.toByteArray(); + return WebResponseUtils.bytesToWebResponse( + result, + file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf"); + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java b/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java index 9551754a..f6099c3a 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java @@ -1,11 +1,13 @@ package stirling.software.SPDF.controller.api; + import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.ArrayList; + import org.apache.pdfbox.multipdf.Overlay; import org.apache.pdfbox.pdmodel.PDDocument; import org.springframework.http.MediaType; @@ -18,36 +20,49 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.general.OverlayPdfsRequest; import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/general") @Tag(name = "General", description = "General APIs") public class PdfOverlayController { - @PostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data") - @Operation(summary = "Overlay PDF files in various modes", description = "Overlay PDF files onto a base PDF with different modes: Sequential, Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO") - public ResponseEntity overlayPdfs(@ModelAttribute OverlayPdfsRequest request) throws IOException { + @PostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data") + @Operation( + summary = "Overlay PDF files in various modes", + description = + "Overlay PDF files onto a base PDF with different modes: Sequential, Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO") + public ResponseEntity overlayPdfs(@ModelAttribute OverlayPdfsRequest request) + throws IOException { MultipartFile baseFile = request.getFileInput(); int overlayPos = request.getOverlayPosition(); - + MultipartFile[] overlayFiles = request.getOverlayFiles(); File[] overlayPdfFiles = new File[overlayFiles.length]; List tempFiles = new ArrayList<>(); // List to keep track of temporary files - try { + try { for (int i = 0; i < overlayFiles.length; i++) { overlayPdfFiles[i] = GeneralUtils.multipartToFile(overlayFiles[i]); } - - String mode = request.getOverlayMode(); // "SequentialOverlay", "InterleavedOverlay", "FixedRepeatOverlay" + + String mode = request.getOverlayMode(); // "SequentialOverlay", "InterleavedOverlay", + // "FixedRepeatOverlay" int[] counts = request.getCounts(); // Used for FixedRepeatOverlay mode try (PDDocument basePdf = PDDocument.load(baseFile.getInputStream()); Overlay overlay = new Overlay()) { - Map overlayGuide = prepareOverlayGuide(basePdf.getNumberOfPages(), overlayPdfFiles, mode, counts, tempFiles); - + Map overlayGuide = + prepareOverlayGuide( + basePdf.getNumberOfPages(), + overlayPdfFiles, + mode, + counts, + tempFiles); + overlay.setInputPDF(basePdf); if (overlayPos == 0) { overlay.setOverlayPosition(Overlay.Position.FOREGROUND); @@ -58,10 +73,13 @@ public class PdfOverlayController { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); overlay.overlay(overlayGuide).save(outputStream); byte[] data = outputStream.toByteArray(); - String outputFilename = baseFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf"; // Remove file extension and append .pdf - - return WebResponseUtils.bytesToWebResponse(data, outputFilename, MediaType.APPLICATION_PDF); - } + String outputFilename = + baseFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_overlayed.pdf"; // Remove file extension and append .pdf + + return WebResponseUtils.bytesToWebResponse( + data, outputFilename, MediaType.APPLICATION_PDF); + } } finally { for (File overlayPdfFile : overlayPdfFiles) { if (overlayPdfFile != null) { @@ -76,7 +94,9 @@ public class PdfOverlayController { } } - private Map prepareOverlayGuide(int basePageCount, File[] overlayFiles, String mode, int[] counts, List tempFiles) throws IOException { + private Map prepareOverlayGuide( + int basePageCount, File[] overlayFiles, String mode, int[] counts, List tempFiles) + throws IOException { Map overlayGuide = new HashMap<>(); switch (mode) { case "SequentialOverlay": @@ -94,12 +114,19 @@ public class PdfOverlayController { return overlayGuide; } - private void sequentialOverlay(Map overlayGuide, File[] overlayFiles, int basePageCount, List tempFiles) throws IOException { + private void sequentialOverlay( + Map overlayGuide, + File[] overlayFiles, + int basePageCount, + List tempFiles) + throws IOException { int overlayFileIndex = 0; int pageCountInCurrentOverlay = 0; for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) { - if (pageCountInCurrentOverlay == 0 || pageCountInCurrentOverlay >= getNumberOfPages(overlayFiles[overlayFileIndex])) { + if (pageCountInCurrentOverlay == 0 + || pageCountInCurrentOverlay + >= getNumberOfPages(overlayFiles[overlayFileIndex])) { pageCountInCurrentOverlay = 0; overlayFileIndex = (overlayFileIndex + 1) % overlayFiles.length; } @@ -125,13 +152,9 @@ public class PdfOverlayController { } } - - - - - - - private void interleavedOverlay(Map overlayGuide, File[] overlayFiles, int basePageCount) throws IOException { + private void interleavedOverlay( + Map overlayGuide, File[] overlayFiles, int basePageCount) + throws IOException { for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) { File overlayFile = overlayFiles[(basePageIndex - 1) % overlayFiles.length]; @@ -145,10 +168,12 @@ public class PdfOverlayController { } } - - private void fixedRepeatOverlay(Map overlayGuide, File[] overlayFiles, int[] counts, int basePageCount) throws IOException { + private void fixedRepeatOverlay( + Map overlayGuide, File[] overlayFiles, int[] counts, int basePageCount) + throws IOException { if (overlayFiles.length != counts.length) { - throw new IllegalArgumentException("Counts array length must match the number of overlay files"); + throw new IllegalArgumentException( + "Counts array length must match the number of overlay files"); } int currentPage = 1; for (int i = 0; i < overlayFiles.length; i++) { @@ -167,7 +192,7 @@ public class PdfOverlayController { } } } - } -// Additional classes like OverlayPdfsRequest, WebResponseUtils, etc. are assumed to be defined elsewhere. +// Additional classes like OverlayPdfsRequest, WebResponseUtils, etc. are assumed to be defined +// elsewhere. diff --git a/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java b/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java index b20bd99b..8c31bf3c 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java @@ -17,200 +17,204 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.SortTypes; import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.model.api.general.RearrangePagesRequest; import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/general") @Tag(name = "General", description = "General APIs") public class RearrangePagesPDFController { - private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class); + private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class); - @PostMapping(consumes = "multipart/form-data", value = "/remove-pages") - @Operation(summary = "Remove pages from a PDF file", description = "This endpoint removes specified pages from a given PDF file. Users can provide a comma-separated list of page numbers or ranges to delete. Input:PDF Output:PDF Type:SISO") - public ResponseEntity deletePages(@ModelAttribute PDFWithPageNums request ) - throws IOException { + @PostMapping(consumes = "multipart/form-data", value = "/remove-pages") + @Operation( + summary = "Remove pages from a PDF file", + description = + "This endpoint removes specified pages from a given PDF file. Users can provide a comma-separated list of page numbers or ranges to delete. Input:PDF Output:PDF Type:SISO") + public ResponseEntity deletePages(@ModelAttribute PDFWithPageNums request) + throws IOException { - MultipartFile pdfFile = request.getFileInput(); - String pagesToDelete = request.getPageNumbers(); - - PDDocument document = PDDocument.load(pdfFile.getBytes()); + MultipartFile pdfFile = request.getFileInput(); + String pagesToDelete = request.getPageNumbers(); - // Split the page order string into an array of page numbers or range of numbers - String[] pageOrderArr = pagesToDelete.split(","); + PDDocument document = PDDocument.load(pdfFile.getBytes()); - List pagesToRemove = GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); + // Split the page order string into an array of page numbers or range of numbers + String[] pageOrderArr = pagesToDelete.split(","); - for (int i = pagesToRemove.size() - 1; i >= 0; i--) { - int pageIndex = pagesToRemove.get(i); - document.removePage(pageIndex); - } - return WebResponseUtils.pdfDocToWebResponse(document, - pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf"); + List pagesToRemove = + GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); - } - - - - private List removeFirst(int totalPages) { - if (totalPages <= 1) - return new ArrayList<>(); - List newPageOrder = new ArrayList<>(); - for (int i = 2; i <= totalPages; i++) { - newPageOrder.add(i - 1); - } - return newPageOrder; - } - - private List removeLast(int totalPages) { - if (totalPages <= 1) - return new ArrayList<>(); - List newPageOrder = new ArrayList<>(); - for (int i = 1; i < totalPages; i++) { - newPageOrder.add(i - 1); - } - return newPageOrder; - } - - private List removeFirstAndLast(int totalPages) { - if (totalPages <= 2) - return new ArrayList<>(); - List newPageOrder = new ArrayList<>(); - for (int i = 2; i < totalPages; i++) { - newPageOrder.add(i - 1); - } - return newPageOrder; - } - - private List reverseOrder(int totalPages) { - List newPageOrder = new ArrayList<>(); - for (int i = totalPages; i >= 1; i--) { - newPageOrder.add(i - 1); - } - return newPageOrder; - } - - private List duplexSort(int totalPages) { - List newPageOrder = new ArrayList<>(); - int half = (totalPages + 1) / 2; // This ensures proper behavior with odd numbers of pages - for (int i = 1; i <= half; i++) { - newPageOrder.add(i - 1); - if (i <= totalPages - half) { // Avoid going out of bounds - newPageOrder.add(totalPages - i); - } - } - return newPageOrder; - } - - private List bookletSort(int totalPages) { - List newPageOrder = new ArrayList<>(); - for (int i = 0; i < totalPages / 2; i++) { - newPageOrder.add(i); - newPageOrder.add(totalPages - i - 1); - } - return newPageOrder; - } - - private List sideStitchBooklet(int totalPages) { - List newPageOrder = new ArrayList<>(); - for (int i = 0; i < (totalPages + 3) / 4; i++) { - int begin = i * 4; - newPageOrder.add(Math.min(begin + 3, totalPages - 1)); - newPageOrder.add(Math.min(begin, totalPages - 1)); - newPageOrder.add(Math.min(begin + 1, totalPages - 1)); - newPageOrder.add(Math.min(begin + 2, totalPages - 1)); - } - return newPageOrder; + for (int i = pagesToRemove.size() - 1; i >= 0; i--) { + int pageIndex = pagesToRemove.get(i); + document.removePage(pageIndex); + } + return WebResponseUtils.pdfDocToWebResponse( + document, + pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf"); } - private List oddEvenSplit(int totalPages) { - List newPageOrder = new ArrayList<>(); - for (int i = 1; i <= totalPages; i += 2) { - newPageOrder.add(i - 1); - } - for (int i = 2; i <= totalPages; i += 2) { - newPageOrder.add(i - 1); - } - return newPageOrder; - } + private List removeFirst(int totalPages) { + if (totalPages <= 1) return new ArrayList<>(); + List newPageOrder = new ArrayList<>(); + for (int i = 2; i <= totalPages; i++) { + newPageOrder.add(i - 1); + } + return newPageOrder; + } - private List processSortTypes(String sortTypes, int totalPages) { - try { - SortTypes mode = SortTypes.valueOf(sortTypes.toUpperCase()); - switch (mode) { - case REVERSE_ORDER: - return reverseOrder(totalPages); - case DUPLEX_SORT: - return duplexSort(totalPages); - case BOOKLET_SORT: - return bookletSort(totalPages); - case SIDE_STITCH_BOOKLET_SORT: - return sideStitchBooklet(totalPages); - case ODD_EVEN_SPLIT: - return oddEvenSplit(totalPages); - case REMOVE_FIRST: - return removeFirst(totalPages); - case REMOVE_LAST: - return removeLast(totalPages); - case REMOVE_FIRST_AND_LAST: - return removeFirstAndLast(totalPages); - default: - throw new IllegalArgumentException("Unsupported custom mode"); - } - } catch (IllegalArgumentException e) { - logger.error("Unsupported custom mode", e); - return null; - } - } + private List removeLast(int totalPages) { + if (totalPages <= 1) return new ArrayList<>(); + List newPageOrder = new ArrayList<>(); + for (int i = 1; i < totalPages; i++) { + newPageOrder.add(i - 1); + } + return newPageOrder; + } - @PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages") - @Operation(summary = "Rearrange pages in a PDF file", description = "This endpoint rearranges pages in a given PDF file based on the specified page order or custom mode. Users can provide a page order as a comma-separated list of page numbers or page ranges, or a custom mode. Input:PDF Output:PDF") - public ResponseEntity rearrangePages(@ModelAttribute RearrangePagesRequest request) throws IOException { - MultipartFile pdfFile = request.getFileInput(); - String pageOrder = request.getPageNumbers(); - String sortType = request.getCustomMode(); - try { - // Load the input PDF - PDDocument document = PDDocument.load(pdfFile.getInputStream()); + private List removeFirstAndLast(int totalPages) { + if (totalPages <= 2) return new ArrayList<>(); + List newPageOrder = new ArrayList<>(); + for (int i = 2; i < totalPages; i++) { + newPageOrder.add(i - 1); + } + return newPageOrder; + } - // Split the page order string into an array of page numbers or range of numbers - String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0]; - int totalPages = document.getNumberOfPages(); - List newPageOrder; - if (sortType != null && sortType.length() > 0) { - newPageOrder = processSortTypes(sortType, totalPages); - } else { - newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages); - } - logger.info("newPageOrder = " +newPageOrder); - logger.info("totalPages = " +totalPages); - // Create a new list to hold the pages in the new order - List newPages = new ArrayList<>(); - for (int i = 0; i < newPageOrder.size(); i++) { - newPages.add(document.getPage(newPageOrder.get(i))); - } + private List reverseOrder(int totalPages) { + List newPageOrder = new ArrayList<>(); + for (int i = totalPages; i >= 1; i--) { + newPageOrder.add(i - 1); + } + return newPageOrder; + } - // Remove all the pages from the original document - for (int i = document.getNumberOfPages() - 1; i >= 0; i--) { - document.removePage(i); - } + private List duplexSort(int totalPages) { + List newPageOrder = new ArrayList<>(); + int half = (totalPages + 1) / 2; // This ensures proper behavior with odd numbers of pages + for (int i = 1; i <= half; i++) { + newPageOrder.add(i - 1); + if (i <= totalPages - half) { // Avoid going out of bounds + newPageOrder.add(totalPages - i); + } + } + return newPageOrder; + } - // Add the pages in the new order - for (PDPage page : newPages) { - document.addPage(page); - } + private List bookletSort(int totalPages) { + List newPageOrder = new ArrayList<>(); + for (int i = 0; i < totalPages / 2; i++) { + newPageOrder.add(i); + newPageOrder.add(totalPages - i - 1); + } + return newPageOrder; + } - return WebResponseUtils.pdfDocToWebResponse(document, - pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rearranged.pdf"); - } catch (IOException e) { - logger.error("Failed rearranging documents", e); - return null; - } - } + private List sideStitchBooklet(int totalPages) { + List newPageOrder = new ArrayList<>(); + for (int i = 0; i < (totalPages + 3) / 4; i++) { + int begin = i * 4; + newPageOrder.add(Math.min(begin + 3, totalPages - 1)); + newPageOrder.add(Math.min(begin, totalPages - 1)); + newPageOrder.add(Math.min(begin + 1, totalPages - 1)); + newPageOrder.add(Math.min(begin + 2, totalPages - 1)); + } + return newPageOrder; + } - + private List oddEvenSplit(int totalPages) { + List newPageOrder = new ArrayList<>(); + for (int i = 1; i <= totalPages; i += 2) { + newPageOrder.add(i - 1); + } + for (int i = 2; i <= totalPages; i += 2) { + newPageOrder.add(i - 1); + } + return newPageOrder; + } + private List processSortTypes(String sortTypes, int totalPages) { + try { + SortTypes mode = SortTypes.valueOf(sortTypes.toUpperCase()); + switch (mode) { + case REVERSE_ORDER: + return reverseOrder(totalPages); + case DUPLEX_SORT: + return duplexSort(totalPages); + case BOOKLET_SORT: + return bookletSort(totalPages); + case SIDE_STITCH_BOOKLET_SORT: + return sideStitchBooklet(totalPages); + case ODD_EVEN_SPLIT: + return oddEvenSplit(totalPages); + case REMOVE_FIRST: + return removeFirst(totalPages); + case REMOVE_LAST: + return removeLast(totalPages); + case REMOVE_FIRST_AND_LAST: + return removeFirstAndLast(totalPages); + default: + throw new IllegalArgumentException("Unsupported custom mode"); + } + } catch (IllegalArgumentException e) { + logger.error("Unsupported custom mode", e); + return null; + } + } + + @PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages") + @Operation( + summary = "Rearrange pages in a PDF file", + description = + "This endpoint rearranges pages in a given PDF file based on the specified page order or custom mode. Users can provide a page order as a comma-separated list of page numbers or page ranges, or a custom mode. Input:PDF Output:PDF") + public ResponseEntity rearrangePages(@ModelAttribute RearrangePagesRequest request) + throws IOException { + MultipartFile pdfFile = request.getFileInput(); + String pageOrder = request.getPageNumbers(); + String sortType = request.getCustomMode(); + try { + // Load the input PDF + PDDocument document = PDDocument.load(pdfFile.getInputStream()); + + // Split the page order string into an array of page numbers or range of numbers + String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0]; + int totalPages = document.getNumberOfPages(); + List newPageOrder; + if (sortType != null && sortType.length() > 0) { + newPageOrder = processSortTypes(sortType, totalPages); + } else { + newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages); + } + logger.info("newPageOrder = " + newPageOrder); + logger.info("totalPages = " + totalPages); + // Create a new list to hold the pages in the new order + List newPages = new ArrayList<>(); + for (int i = 0; i < newPageOrder.size(); i++) { + newPages.add(document.getPage(newPageOrder.get(i))); + } + + // Remove all the pages from the original document + for (int i = document.getNumberOfPages() - 1; i >= 0; i--) { + document.removePage(i); + } + + // Add the pages in the new order + for (PDPage page : newPages) { + document.addPage(page); + } + + return WebResponseUtils.pdfDocToWebResponse( + document, + pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_rearranged.pdf"); + } catch (IOException e) { + logger.error("Failed rearranging documents", e); + return null; + } + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/RotationController.java b/src/main/java/stirling/software/SPDF/controller/api/RotationController.java index ed527549..883beb5d 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/RotationController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/RotationController.java @@ -16,6 +16,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.general.RotatePDFRequest; import stirling.software.SPDF.utils.WebResponseUtils; @@ -28,11 +29,11 @@ public class RotationController { @PostMapping(consumes = "multipart/form-data", value = "/rotate-pdf") @Operation( - summary = "Rotate a PDF file", - description = "This endpoint rotates a given PDF file by a specified angle. The angle must be a multiple of 90. Input:PDF Output:PDF Type:SISO" - ) - public ResponseEntity rotatePDF( - @ModelAttribute RotatePDFRequest request) throws IOException { + summary = "Rotate a PDF file", + description = + "This endpoint rotates a given PDF file by a specified angle. The angle must be a multiple of 90. Input:PDF Output:PDF Type:SISO") + public ResponseEntity rotatePDF(@ModelAttribute RotatePDFRequest request) + throws IOException { MultipartFile pdfFile = request.getFileInput(); Integer angle = request.getAngle(); // Load the PDF document @@ -45,8 +46,8 @@ public class RotationController { page.setRotation(page.getRotation() + angle); } - return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rotated.pdf"); - + return WebResponseUtils.pdfDocToWebResponse( + document, + pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rotated.pdf"); } - } 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 743ea6b1..0dcec05c 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java @@ -23,88 +23,90 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.general.ScalePagesRequest; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/general") @Tag(name = "General", description = "General APIs") public class ScalePagesController { - private static final Logger logger = LoggerFactory.getLogger(ScalePagesController.class); + private static final Logger logger = LoggerFactory.getLogger(ScalePagesController.class); - @PostMapping(value = "/scale-pages", consumes = "multipart/form-data") - @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(@ModelAttribute ScalePagesRequest request) throws IOException { - MultipartFile file = request.getFileInput(); - String targetPDRectangle = request.getPageSize(); - float scaleFactor = request.getScaleFactor(); + @PostMapping(value = "/scale-pages", consumes = "multipart/form-data") + @Operation( + summary = "Change the size of a PDF page/document", + description = + "This operation takes an input PDF file and the size to scale the pages to in the output PDF file. Input:PDF Output:PDF Type:SISO") + public ResponseEntity scalePages(@ModelAttribute ScalePagesRequest request) + throws IOException { + MultipartFile file = request.getFileInput(); + String targetPDRectangle = request.getPageSize(); + float scaleFactor = request.getScaleFactor(); - Map sizeMap = new HashMap<>(); - // Add A0 - A10 - sizeMap.put("A0", PDRectangle.A0); - sizeMap.put("A1", PDRectangle.A1); - sizeMap.put("A2", PDRectangle.A2); - sizeMap.put("A3", PDRectangle.A3); - sizeMap.put("A4", PDRectangle.A4); - sizeMap.put("A5", PDRectangle.A5); - sizeMap.put("A6", PDRectangle.A6); + Map sizeMap = new HashMap<>(); + // Add A0 - A10 + sizeMap.put("A0", PDRectangle.A0); + sizeMap.put("A1", PDRectangle.A1); + sizeMap.put("A2", PDRectangle.A2); + sizeMap.put("A3", PDRectangle.A3); + sizeMap.put("A4", PDRectangle.A4); + sizeMap.put("A5", PDRectangle.A5); + sizeMap.put("A6", PDRectangle.A6); - // Add other sizes - sizeMap.put("LETTER", PDRectangle.LETTER); - sizeMap.put("LEGAL", PDRectangle.LEGAL); + // Add other sizes + sizeMap.put("LETTER", PDRectangle.LETTER); + sizeMap.put("LEGAL", PDRectangle.LEGAL); - if (!sizeMap.containsKey(targetPDRectangle)) { - throw new IllegalArgumentException( - "Invalid PDRectangle. It must be one of the following: A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10"); - } + if (!sizeMap.containsKey(targetPDRectangle)) { + throw new IllegalArgumentException( + "Invalid PDRectangle. It must be one of the following: A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10"); + } - PDRectangle targetSize = sizeMap.get(targetPDRectangle); + PDRectangle targetSize = sizeMap.get(targetPDRectangle); - PDDocument sourceDocument = PDDocument.load(file.getBytes()); - PDDocument outputDocument = new PDDocument(); + PDDocument sourceDocument = PDDocument.load(file.getBytes()); + PDDocument outputDocument = new PDDocument(); - int totalPages = sourceDocument.getNumberOfPages(); - for (int i = 0; i < totalPages; i++) { - PDPage sourcePage = sourceDocument.getPage(i); - PDRectangle sourceSize = sourcePage.getMediaBox(); - - float scaleWidth = targetSize.getWidth() / sourceSize.getWidth(); - float scaleHeight = targetSize.getHeight() / sourceSize.getHeight(); - float scale = Math.min(scaleWidth, scaleHeight) * scaleFactor; - - PDPage newPage = new PDPage(targetSize); - outputDocument.addPage(newPage); - - PDPageContentStream contentStream = new PDPageContentStream(outputDocument, newPage, PDPageContentStream.AppendMode.APPEND, true); - - float x = (targetSize.getWidth() - sourceSize.getWidth() * scale) / 2; - float y = (targetSize.getHeight() - sourceSize.getHeight() * scale) / 2; - - contentStream.saveGraphicsState(); - contentStream.transform(Matrix.getTranslateInstance(x, y)); - contentStream.transform(Matrix.getScaleInstance(scale, scale)); - - LayerUtility layerUtility = new LayerUtility(outputDocument); - PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, i); - contentStream.drawForm(form); + int totalPages = sourceDocument.getNumberOfPages(); + for (int i = 0; i < totalPages; i++) { + PDPage sourcePage = sourceDocument.getPage(i); + PDRectangle sourceSize = sourcePage.getMediaBox(); - contentStream.restoreGraphicsState(); - contentStream.close(); - } + float scaleWidth = targetSize.getWidth() / sourceSize.getWidth(); + float scaleHeight = targetSize.getHeight() / sourceSize.getHeight(); + float scale = Math.min(scaleWidth, scaleHeight) * scaleFactor; + PDPage newPage = new PDPage(targetSize); + outputDocument.addPage(newPage); + PDPageContentStream contentStream = + new PDPageContentStream( + outputDocument, newPage, PDPageContentStream.AppendMode.APPEND, true); + float x = (targetSize.getWidth() - sourceSize.getWidth() * scale) / 2; + float y = (targetSize.getHeight() - sourceSize.getHeight() * scale) / 2; + contentStream.saveGraphicsState(); + contentStream.transform(Matrix.getTranslateInstance(x, y)); + contentStream.transform(Matrix.getScaleInstance(scale, scale)); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - outputDocument.save(baos); - outputDocument.close(); - sourceDocument.close(); - - - return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), - file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf"); - } + LayerUtility layerUtility = new LayerUtility(outputDocument); + PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, i); + contentStream.drawForm(form); + contentStream.restoreGraphicsState(); + contentStream.close(); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + outputDocument.save(baos); + outputDocument.close(); + sourceDocument.close(); + + return WebResponseUtils.bytesToWebResponse( + baos.toByteArray(), + file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf"); + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java b/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java index 0651949e..a521769e 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java @@ -25,6 +25,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.utils.WebResponseUtils; @@ -36,19 +37,24 @@ public class SplitPDFController { private static final Logger logger = LoggerFactory.getLogger(SplitPDFController.class); @PostMapping(consumes = "multipart/form-data", value = "/split-pages") - @Operation(summary = "Split a PDF file into separate documents", - description = "This endpoint splits a given PDF file into separate documents based on the specified page numbers or ranges. Users can specify pages using individual numbers, ranges, or 'all' for every page. Input:PDF Output:PDF Type:SIMO") - public ResponseEntity splitPdf(@ModelAttribute PDFWithPageNums request) throws IOException { - MultipartFile file = request.getFileInput(); + @Operation( + summary = "Split a PDF file into separate documents", + description = + "This endpoint splits a given PDF file into separate documents based on the specified page numbers or ranges. Users can specify pages using individual numbers, ranges, or 'all' for every page. Input:PDF Output:PDF Type:SIMO") + public ResponseEntity splitPdf(@ModelAttribute PDFWithPageNums request) + throws IOException { + MultipartFile file = request.getFileInput(); String pages = request.getPageNumbers(); // open the pdf document InputStream inputStream = file.getInputStream(); PDDocument document = PDDocument.load(inputStream); List pageNumbers = request.getPageNumbersList(document); - if(!pageNumbers.contains(document.getNumberOfPages() - 1)) - pageNumbers.add(document.getNumberOfPages()- 1); - logger.info("Splitting PDF into pages: {}", pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(","))); + if (!pageNumbers.contains(document.getNumberOfPages() - 1)) + pageNumbers.add(document.getNumberOfPages() - 1); + logger.info( + "Splitting PDF into pages: {}", + pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(","))); // split the document List splitDocumentsBoas = new ArrayList<>(); @@ -72,7 +78,6 @@ public class SplitPDFController { } } - // closing the original document document.close(); @@ -104,8 +109,7 @@ public class SplitPDFController { Files.delete(zipFile); // return the Resource in the response - return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); - + return WebResponseUtils.bytesToWebResponse( + data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); } - -} \ No newline at end of file +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java index f36f64da..5d96920f 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.controller.api; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Files; @@ -25,17 +26,22 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/general") @Tag(name = "General", description = "General APIs") public class SplitPdfBySectionsController { - - @PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data") - @Operation(summary = "Split PDF pages into smaller sections", description = "Split each page of a PDF into smaller sections based on the user's choice (halves, thirds, quarters, etc.), both vertically and horizontally. Input:PDF Output:ZIP-PDF Type:SISO") - public ResponseEntity splitPdf(@ModelAttribute SplitPdfBySectionsRequest request) throws Exception { + @PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data") + @Operation( + summary = "Split PDF pages into smaller sections", + description = + "Split each page of a PDF into smaller sections based on the user's choice (halves, thirds, quarters, etc.), both vertically and horizontally. Input:PDF Output:ZIP-PDF Type:SISO") + public ResponseEntity splitPdf(@ModelAttribute SplitPdfBySectionsRequest request) + throws Exception { List splitDocumentsBoas = new ArrayList<>(); MultipartFile file = request.getFileInput(); @@ -59,8 +65,6 @@ public class SplitPdfBySectionsController { String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", ""); byte[] data; - - try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) { int pageNum = 1; for (int i = 0; i < splitDocumentsBoas.size(); i++) { @@ -82,10 +86,13 @@ public class SplitPdfBySectionsController { Files.delete(zipFile); } - return WebResponseUtils.bytesToWebResponse(data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.bytesToWebResponse( + data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM); } - - public List splitPdfPages(PDDocument document, int horizontalDivisions, int verticalDivisions) throws IOException { + + public List splitPdfPages( + PDDocument document, int horizontalDivisions, int verticalDivisions) + throws IOException { List splitDocuments = new ArrayList<>(); for (PDPage originalPage : document.getPages()) { @@ -103,9 +110,12 @@ public class SplitPdfBySectionsController { PDPage subPage = new PDPage(new PDRectangle(subPageWidth, subPageHeight)); subDoc.addPage(subPage); - PDFormXObject form = layerUtility.importPageAsForm(document, document.getPages().indexOf(originalPage)); + PDFormXObject form = + layerUtility.importPageAsForm( + document, document.getPages().indexOf(originalPage)); - try (PDPageContentStream contentStream = new PDPageContentStream(subDoc, subPage)) { + try (PDPageContentStream contentStream = + new PDPageContentStream(subDoc, subPage)) { // Set clipping area and position float translateX = -subPageWidth * i; float translateY = height - subPageHeight * (verticalDivisions - j); @@ -127,9 +137,4 @@ public class SplitPdfBySectionsController { return splitDocuments; } - - - - - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java index 9b25e8be..28ac4673 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.controller.api; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Files; @@ -20,6 +21,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest; import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.WebResponseUtils; @@ -29,22 +31,23 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "General", description = "General APIs") public class SplitPdfBySizeController { - @PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data") - @Operation(summary = "Auto split PDF pages into separate documents based on size or count", description = "split PDF into multiple paged documents based on size/count, ie if 20 pages and split into 5, it does 5 documents each 4 pages\r\n" - + " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB (rounded so that it accepts 1.9MB but not 2.1MB) Input:PDF Output:ZIP-PDF Type:SISO") - public ResponseEntity autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request) throws Exception { - List splitDocumentsBoas = new ArrayList(); - - - + @Operation( + summary = "Auto split PDF pages into separate documents based on size or count", + description = + "split PDF into multiple paged documents based on size/count, ie if 20 pages and split into 5, it does 5 documents each 4 pages\r\n" + + " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB (rounded so that it accepts 1.9MB but not 2.1MB) Input:PDF Output:ZIP-PDF Type:SISO") + public ResponseEntity autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request) + throws Exception { + List splitDocumentsBoas = new ArrayList(); + MultipartFile file = request.getFileInput(); PDDocument sourceDocument = PDDocument.load(file.getInputStream()); - - //0 = size, 1 = page count, 2 = doc count + + // 0 = size, 1 = page count, 2 = doc count int type = request.getSplitType(); String value = request.getSplitValue(); - + if (type == 0) { // Split by size long maxBytes = GeneralUtils.convertSizeToBytes(value); long currentSize = 0; @@ -93,7 +96,7 @@ public class SplitPdfBySizeController { splitDocumentsBoas.add(currentDocToByteArray(currentDoc)); } } else if (type == 2) { // Split by doc count - int documentCount = Integer.parseInt(value); + int documentCount = Integer.parseInt(value); int totalPageCount = sourceDocument.getNumberOfPages(); int pagesPerDocument = totalPageCount / documentCount; int extraPages = totalPageCount % documentCount; @@ -114,9 +117,7 @@ public class SplitPdfBySizeController { } sourceDocument.close(); - - - + Path zipFile = Files.createTempFile("split_documents", ".zip"); String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", ""); byte[] data; @@ -135,19 +136,18 @@ public class SplitPdfBySizeController { } catch (Exception e) { e.printStackTrace(); } finally { - data = Files.readAllBytes(zipFile); + data = Files.readAllBytes(zipFile); Files.delete(zipFile); } - return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.bytesToWebResponse( + data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); } - + private ByteArrayOutputStream currentDocToByteArray(PDDocument document) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); document.save(baos); document.close(); return baos; } - - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java b/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java index 22bf1d70..cd971b55 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java @@ -20,8 +20,10 @@ import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/general") @Tag(name = "General", description = "General APIs") @@ -29,58 +31,61 @@ public class ToSinglePageController { private static final Logger logger = LoggerFactory.getLogger(ToSinglePageController.class); - @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-single-page") @Operation( - summary = "Convert a multi-page PDF into a single long page PDF", - description = "This endpoint converts a multi-page PDF document into a single paged PDF document. The width of the single page will be same as the input's width, but the height will be the sum of all the pages' heights. Input:PDF Output:PDF Type:SISO" - ) - public ResponseEntity pdfToSinglePage(@ModelAttribute PDFFile request) throws IOException { + summary = "Convert a multi-page PDF into a single long page PDF", + description = + "This endpoint converts a multi-page PDF document into a single paged PDF document. The width of the single page will be same as the input's width, but the height will be the sum of all the pages' heights. Input:PDF Output:PDF Type:SISO") + public ResponseEntity pdfToSinglePage(@ModelAttribute PDFFile request) + throws IOException { - // Load the source document - PDDocument sourceDocument = PDDocument.load(request.getFileInput().getInputStream()); + // Load the source document + PDDocument sourceDocument = PDDocument.load(request.getFileInput().getInputStream()); - // Calculate total height and max width - float totalHeight = 0; - float maxWidth = 0; - for (PDPage page : sourceDocument.getPages()) { - PDRectangle pageSize = page.getMediaBox(); - totalHeight += pageSize.getHeight(); - maxWidth = Math.max(maxWidth, pageSize.getWidth()); - } + // Calculate total height and max width + float totalHeight = 0; + float maxWidth = 0; + for (PDPage page : sourceDocument.getPages()) { + PDRectangle pageSize = page.getMediaBox(); + totalHeight += pageSize.getHeight(); + maxWidth = Math.max(maxWidth, pageSize.getWidth()); + } - // Create new document and page with calculated dimensions - PDDocument newDocument = new PDDocument(); - PDPage newPage = new PDPage(new PDRectangle(maxWidth, totalHeight)); - newDocument.addPage(newPage); + // Create new document and page with calculated dimensions + PDDocument newDocument = new PDDocument(); + PDPage newPage = new PDPage(new PDRectangle(maxWidth, totalHeight)); + newDocument.addPage(newPage); - // Initialize the content stream of the new page - PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage); - contentStream.close(); - - LayerUtility layerUtility = new LayerUtility(newDocument); - float yOffset = totalHeight; + // Initialize the content stream of the new page + PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage); + contentStream.close(); - // For each page, copy its content to the new page at the correct offset - for (PDPage page : sourceDocument.getPages()) { - PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, sourceDocument.getPages().indexOf(page)); - AffineTransform af = AffineTransform.getTranslateInstance(0, yOffset - page.getMediaBox().getHeight()); - layerUtility.wrapInSaveRestore(newPage); - String defaultLayerName = "Layer" + sourceDocument.getPages().indexOf(page); - layerUtility.appendFormAsLayer(newPage, form, af, defaultLayerName); - yOffset -= page.getMediaBox().getHeight(); - } + LayerUtility layerUtility = new LayerUtility(newDocument); + float yOffset = totalHeight; - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - newDocument.save(baos); - newDocument.close(); - sourceDocument.close(); + // For each page, copy its content to the new page at the correct offset + for (PDPage page : sourceDocument.getPages()) { + PDFormXObject form = + layerUtility.importPageAsForm( + sourceDocument, sourceDocument.getPages().indexOf(page)); + AffineTransform af = + AffineTransform.getTranslateInstance( + 0, yOffset - page.getMediaBox().getHeight()); + layerUtility.wrapInSaveRestore(newPage); + String defaultLayerName = "Layer" + sourceDocument.getPages().indexOf(page); + layerUtility.appendFormAsLayer(newPage, form, af, defaultLayerName); + yOffset -= page.getMediaBox().getHeight(); + } - byte[] result = baos.toByteArray(); - return WebResponseUtils.bytesToWebResponse(result, request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_singlePage.pdf"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + newDocument.save(baos); + newDocument.close(); + sourceDocument.close(); - - - + byte[] result = baos.toByteArray(); + return WebResponseUtils.bytesToWebResponse( + result, + request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_singlePage.pdf"); } -} \ No newline at end of file +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/UserController.java b/src/main/java/stirling/software/SPDF/controller/api/UserController.java index 01a50a3b..89e81c99 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/UserController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/UserController.java @@ -29,14 +29,14 @@ import stirling.software.SPDF.model.User; @Controller @RequestMapping("/api/v1/user") public class UserController { - - @Autowired - private UserService userService; - + + @Autowired private UserService userService; + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/register") - public String register(@RequestParam String username, @RequestParam String password, Model model) { - if(userService.usernameExists(username)) { + public String register( + @RequestParam String username, @RequestParam String password, Model model) { + if (userService.usernameExists(username)) { model.addAttribute("error", "Username already exists"); return "register"; } @@ -44,39 +44,41 @@ public class UserController { userService.saveUser(username, password); return "redirect:/login?registered=true"; } - + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/change-username-and-password") - public RedirectView changeUsernameAndPassword(Principal principal, - @RequestParam String currentPassword, - @RequestParam String newUsername, - @RequestParam String newPassword, - HttpServletRequest request, - HttpServletResponse response, - RedirectAttributes redirectAttributes) { - if (principal == null) { - return new RedirectView("/change-creds?messageType=notAuthenticated"); - } + public RedirectView changeUsernameAndPassword( + Principal principal, + @RequestParam String currentPassword, + @RequestParam String newUsername, + @RequestParam String newPassword, + HttpServletRequest request, + HttpServletResponse response, + RedirectAttributes redirectAttributes) { + if (principal == null) { + return new RedirectView("/change-creds?messageType=notAuthenticated"); + } - Optional userOpt = userService.findByUsername(principal.getName()); + Optional userOpt = userService.findByUsername(principal.getName()); - if (userOpt == null || userOpt.isEmpty()) { - return new RedirectView("/change-creds?messageType=userNotFound"); - } + if (userOpt == null || userOpt.isEmpty()) { + return new RedirectView("/change-creds?messageType=userNotFound"); + } - User user = userOpt.get(); + User user = userOpt.get(); - if (!userService.isPasswordCorrect(user, currentPassword)) { - return new RedirectView("/change-creds?messageType=incorrectPassword"); - } - - if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) { - return new RedirectView("/change-creds?messageType=usernameExists"); - } + if (!userService.isPasswordCorrect(user, currentPassword)) { + return new RedirectView("/change-creds?messageType=incorrectPassword"); + } + if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) { + return new RedirectView("/change-creds?messageType=usernameExists"); + } userService.changePassword(user, newPassword); - if(newUsername != null && newUsername.length() > 0 && !user.getUsername().equals(newUsername)) { + if (newUsername != null + && newUsername.length() > 0 + && !user.getUsername().equals(newUsername)) { userService.changeUsername(user, newUsername); } userService.changeFirstUse(user, false); @@ -87,36 +89,36 @@ public class UserController { return new RedirectView("/login?messageType=credsUpdated"); } - @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/change-username") - public RedirectView changeUsername(Principal principal, - @RequestParam String currentPassword, - @RequestParam String newUsername, - HttpServletRequest request, - HttpServletResponse response, - RedirectAttributes redirectAttributes) { - if (principal == null) { - return new RedirectView("/account?messageType=notAuthenticated"); - } + public RedirectView changeUsername( + Principal principal, + @RequestParam String currentPassword, + @RequestParam String newUsername, + HttpServletRequest request, + HttpServletResponse response, + RedirectAttributes redirectAttributes) { + if (principal == null) { + return new RedirectView("/account?messageType=notAuthenticated"); + } - Optional userOpt = userService.findByUsername(principal.getName()); + Optional userOpt = userService.findByUsername(principal.getName()); - if (userOpt == null || userOpt.isEmpty()) { - return new RedirectView("/account?messageType=userNotFound"); - } + if (userOpt == null || userOpt.isEmpty()) { + return new RedirectView("/account?messageType=userNotFound"); + } - User user = userOpt.get(); + User user = userOpt.get(); - if (!userService.isPasswordCorrect(user, currentPassword)) { - return new RedirectView("/account?messageType=incorrectPassword"); - } + if (!userService.isPasswordCorrect(user, currentPassword)) { + return new RedirectView("/account?messageType=incorrectPassword"); + } - if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) { - return new RedirectView("/account?messageType=usernameExists"); - } + if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) { + return new RedirectView("/account?messageType=usernameExists"); + } - if(newUsername != null && newUsername.length() > 0) { + if (newUsername != null && newUsername.length() > 0) { userService.changeUsername(user, newUsername); } @@ -125,30 +127,31 @@ public class UserController { return new RedirectView("/login?messageType=credsUpdated"); } - + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/change-password") - public RedirectView changePassword(Principal principal, - @RequestParam String currentPassword, - @RequestParam String newPassword, - HttpServletRequest request, - HttpServletResponse response, - RedirectAttributes redirectAttributes) { - if (principal == null) { - return new RedirectView("/account?messageType=notAuthenticated"); - } + public RedirectView changePassword( + Principal principal, + @RequestParam String currentPassword, + @RequestParam String newPassword, + HttpServletRequest request, + HttpServletResponse response, + RedirectAttributes redirectAttributes) { + if (principal == null) { + return new RedirectView("/account?messageType=notAuthenticated"); + } - Optional userOpt = userService.findByUsername(principal.getName()); + Optional userOpt = userService.findByUsername(principal.getName()); - if (userOpt == null || userOpt.isEmpty()) { - return new RedirectView("/account?messageType=userNotFound"); - } + if (userOpt == null || userOpt.isEmpty()) { + return new RedirectView("/account?messageType=userNotFound"); + } - User user = userOpt.get(); + User user = userOpt.get(); - if (!userService.isPasswordCorrect(user, currentPassword)) { - return new RedirectView("/account?messageType=incorrectPassword"); - } + if (!userService.isPasswordCorrect(user, currentPassword)) { + return new RedirectView("/account?messageType=incorrectPassword"); + } userService.changePassword(user, newPassword); @@ -160,33 +163,37 @@ public class UserController { @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/updateUserSettings") - public String updateUserSettings(HttpServletRequest request, Principal principal) { - Map paramMap = request.getParameterMap(); - Map updates = new HashMap<>(); + public String updateUserSettings(HttpServletRequest request, Principal principal) { + Map paramMap = request.getParameterMap(); + Map updates = new HashMap<>(); - System.out.println("Received parameter map: " + paramMap); + System.out.println("Received parameter map: " + paramMap); - for (Map.Entry entry : paramMap.entrySet()) { - updates.put(entry.getKey(), entry.getValue()[0]); - } + for (Map.Entry entry : paramMap.entrySet()) { + updates.put(entry.getKey(), entry.getValue()[0]); + } - System.out.println("Processed updates: " + updates); + System.out.println("Processed updates: " + updates); - // Assuming you have a method in userService to update the settings for a user - userService.updateUserSettings(principal.getName(), updates); + // Assuming you have a method in userService to update the settings for a user + userService.updateUserSettings(principal.getName(), updates); - return "redirect:/account"; // Redirect to a page of your choice after updating - } + return "redirect:/account"; // Redirect to a page of your choice after updating + } @PreAuthorize("hasRole('ROLE_ADMIN')") @PostMapping("/admin/saveUser") - public RedirectView saveUser(@RequestParam String username, @RequestParam String password, @RequestParam String role, - @RequestParam(name = "forceChange", required = false, defaultValue = "false") boolean forceChange) { - - if(userService.usernameExists(username)) { - return new RedirectView("/addUsers?messageType=usernameExists"); - } - try { + public RedirectView saveUser( + @RequestParam String username, + @RequestParam String password, + @RequestParam String role, + @RequestParam(name = "forceChange", required = false, defaultValue = "false") + boolean forceChange) { + + if (userService.usernameExists(username)) { + return new RedirectView("/addUsers?messageType=usernameExists"); + } + try { // Validate the role Role roleEnum = Role.fromString(role); if (roleEnum == Role.INTERNAL_API_USER) { @@ -197,28 +204,27 @@ public class UserController { // If the role ID is not valid, redirect with an error message return new RedirectView("/addUsers?messageType=invalidRole"); } - + userService.saveUser(username, password, role, forceChange); - return new RedirectView("/addUsers"); // Redirect to account page after adding the user + return new RedirectView("/addUsers"); // Redirect to account page after adding the user } - @PreAuthorize("hasRole('ROLE_ADMIN')") @PostMapping("/admin/deleteUser/{username}") - public String deleteUser(@PathVariable String username, Authentication authentication) { - - // Get the currently authenticated username + public String deleteUser(@PathVariable String username, Authentication authentication) { + + // Get the currently authenticated username String currentUsername = authentication.getName(); // Check if the provided username matches the current session's username if (currentUsername.equals(username)) { throw new IllegalArgumentException("Cannot delete currently logined in user."); } - - userService.deleteUser(username); + + userService.deleteUser(username); return "redirect:/addUsers"; } - + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/get-api-key") public ResponseEntity getApiKey(Principal principal) { @@ -247,6 +253,4 @@ public class UserController { } return ResponseEntity.ok(apiKey); } - - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java index 5839dd2d..bec09040 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java @@ -9,6 +9,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.GeneralFile; import stirling.software.SPDF.utils.FileToPdf; import stirling.software.SPDF.utils.WebResponseUtils; @@ -18,35 +19,30 @@ import stirling.software.SPDF.utils.WebResponseUtils; @RequestMapping("/api/v1/convert") public class ConvertHtmlToPDF { + @PostMapping(consumes = "multipart/form-data", value = "/html/pdf") + @Operation( + summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF", + description = + "This endpoint takes an HTML or ZIP file input and converts it to a PDF format.") + public ResponseEntity HtmlToPdf(@ModelAttribute GeneralFile request) throws Exception { + MultipartFile fileInput = request.getFileInput(); - @PostMapping(consumes = "multipart/form-data", value = "/html/pdf") - @Operation( - summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF", - description = "This endpoint takes an HTML or ZIP file input and converts it to a PDF format." - ) - public ResponseEntity HtmlToPdf( - @ModelAttribute GeneralFile request) - throws Exception { - MultipartFile fileInput = request.getFileInput(); + if (fileInput == null) { + throw new IllegalArgumentException( + "Please provide an HTML or ZIP file for conversion."); + } - if (fileInput == null) { - throw new IllegalArgumentException("Please provide an HTML or ZIP file for conversion."); - } + String originalFilename = fileInput.getOriginalFilename(); + if (originalFilename == null + || (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) { + throw new IllegalArgumentException("File must be either .html or .zip format."); + } + byte[] pdfBytes = FileToPdf.convertHtmlToPdf(fileInput.getBytes(), originalFilename); - String originalFilename = fileInput.getOriginalFilename(); - if (originalFilename == null || (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) { - throw new IllegalArgumentException("File must be either .html or .zip format."); - }byte[] pdfBytes = FileToPdf.convertHtmlToPdf( fileInput.getBytes(), originalFilename); - - String outputFilename = originalFilename.replaceFirst("[.][^.]+$", "") + ".pdf"; // Remove file extension and append .pdf - - return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); - } - - - - - - + String outputFilename = + originalFilename.replaceFirst("[.][^.]+$", "") + + ".pdf"; // Remove file extension and append .pdf + return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java index f0c60b69..a3ea2841 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java @@ -20,6 +20,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.converters.ConvertToImageRequest; import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest; import stirling.software.SPDF.utils.PdfUtils; @@ -33,15 +34,18 @@ public class ConvertImgPDFController { private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class); @PostMapping(consumes = "multipart/form-data", value = "/pdf/img") - @Operation(summary = "Convert PDF to image(s)", - description = "This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images. Input:PDF Output:Image Type:SI-Conditional") - public ResponseEntity convertToImage(@ModelAttribute ConvertToImageRequest request) throws IOException { + @Operation( + summary = "Convert PDF to image(s)", + description = + "This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images. Input:PDF Output:Image Type:SI-Conditional") + public ResponseEntity convertToImage(@ModelAttribute ConvertToImageRequest request) + throws IOException { MultipartFile file = request.getFileInput(); String imageFormat = request.getImageFormat(); String singleOrMultiple = request.getSingleOrMultiple(); String colorType = request.getColorType(); String dpi = request.getDpi(); - + byte[] pdfBytes = file.getBytes(); ImageType colorTypeResult = ImageType.RGB; if ("greyscale".equals(colorType)) { @@ -54,7 +58,14 @@ public class ConvertImgPDFController { byte[] result = null; String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", ""); try { - result = PdfUtils.convertFromPdf(pdfBytes, imageFormat.toUpperCase(), colorTypeResult, singleImage, Integer.valueOf(dpi), filename); + result = + PdfUtils.convertFromPdf( + pdfBytes, + imageFormat.toUpperCase(), + colorTypeResult, + singleImage, + Integer.valueOf(dpi), + filename); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); @@ -65,29 +76,39 @@ public class ConvertImgPDFController { if (singleImage) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.parseMediaType(getMediaType(imageFormat))); - ResponseEntity response = new ResponseEntity<>(new ByteArrayResource(result), headers, HttpStatus.OK); + ResponseEntity response = + new ResponseEntity<>(new ByteArrayResource(result), headers, HttpStatus.OK); return response; } else { ByteArrayResource resource = new ByteArrayResource(result); // return the Resource in the response return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename + "_convertedToImages.zip") - .contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(resource.contentLength()).body(resource); + .header( + HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=" + filename + "_convertedToImages.zip") + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .contentLength(resource.contentLength()) + .body(resource); } } @PostMapping(consumes = "multipart/form-data", value = "/img/pdf") - @Operation(summary = "Convert images to a PDF file", - description = "This endpoint converts one or more images to a PDF file. Users can specify whether to stretch the images to fit the PDF page, and whether to automatically rotate the images. Input:Image Output:PDF Type:SISO?") - public ResponseEntity convertToPdf(@ModelAttribute ConvertToPdfRequest request) throws IOException { + @Operation( + summary = "Convert images to a PDF file", + description = + "This endpoint converts one or more images to a PDF file. Users can specify whether to stretch the images to fit the PDF page, and whether to automatically rotate the images. Input:Image Output:PDF Type:SISO?") + public ResponseEntity convertToPdf(@ModelAttribute ConvertToPdfRequest request) + throws IOException { MultipartFile[] file = request.getFileInput(); String fitOption = request.getFitOption(); String colorType = request.getColorType(); boolean autoRotate = request.isAutoRotate(); - + // Convert the file to PDF and get the resulting bytes byte[] bytes = PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType); - return WebResponseUtils.bytesToWebResponse(bytes, file[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_converted.pdf"); + return WebResponseUtils.bytesToWebResponse( + bytes, + file[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_converted.pdf"); } private String getMediaType(String imageFormat) { diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java index 4191ecdf..8bdc5049 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java @@ -12,6 +12,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.GeneralFile; import stirling.software.SPDF.utils.FileToPdf; import stirling.software.SPDF.utils.WebResponseUtils; @@ -20,17 +21,16 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "Convert", description = "Convert APIs") @RequestMapping("/api/v1/convert") public class ConvertMarkdownToPdf { - - @PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf") + + @PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf") @Operation( - summary = "Convert a Markdown file to PDF", - description = "This endpoint takes a Markdown file input, converts it to HTML, and then to PDF format." - ) - public ResponseEntity markdownToPdf( - @ModelAttribute GeneralFile request) - throws Exception { - MultipartFile fileInput = request.getFileInput(); - + summary = "Convert a Markdown file to PDF", + description = + "This endpoint takes a Markdown file input, converts it to HTML, and then to PDF format.") + public ResponseEntity markdownToPdf(@ModelAttribute GeneralFile request) + throws Exception { + MultipartFile fileInput = request.getFileInput(); + if (fileInput == null) { throw new IllegalArgumentException("Please provide a Markdown file for conversion."); } @@ -45,10 +45,12 @@ public class ConvertMarkdownToPdf { Node document = parser.parse(new String(fileInput.getBytes())); HtmlRenderer renderer = HtmlRenderer.builder().build(); String htmlContent = renderer.render(document); - - byte[] pdfBytes = FileToPdf.convertHtmlToPdf(htmlContent.getBytes(), "converted.html"); - String outputFilename = originalFilename.replaceFirst("[.][^.]+$", "") + ".pdf"; // Remove file extension and append .pdf + byte[] pdfBytes = FileToPdf.convertHtmlToPdf(htmlContent.getBytes(), "converted.html"); + + String outputFilename = + originalFilename.replaceFirst("[.][^.]+$", "") + + ".pdf"; // Remove file extension and append .pdf return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java index e1c18a49..c0008046 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java @@ -18,6 +18,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.GeneralFile; import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; @@ -31,20 +32,33 @@ public class ConvertOfficeController { public byte[] convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException { // Check for valid file extension String originalFilename = inputFile.getOriginalFilename(); - if (originalFilename == null || !isValidFileExtension(FilenameUtils.getExtension(originalFilename))) { + if (originalFilename == null + || !isValidFileExtension(FilenameUtils.getExtension(originalFilename))) { throw new IllegalArgumentException("Invalid file extension"); } // Save the uploaded file to a temporary location - Path tempInputFile = Files.createTempFile("input_", "." + FilenameUtils.getExtension(originalFilename)); + Path tempInputFile = + Files.createTempFile("input_", "." + FilenameUtils.getExtension(originalFilename)); Files.copy(inputFile.getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING); // Prepare the output file path Path tempOutputFile = Files.createTempFile("output_", ".pdf"); // Run the LibreOffice command - List command = new ArrayList<>(Arrays.asList("unoconv", "-vvv", "-f", "pdf", "-o", tempOutputFile.toString(), tempInputFile.toString())); - ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE).runCommandWithOutputHandling(command); + List command = + new ArrayList<>( + Arrays.asList( + "unoconv", + "-vvv", + "-f", + "pdf", + "-o", + tempOutputFile.toString(), + tempInputFile.toString())); + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE) + .runCommandWithOutputHandling(command); // Read the converted PDF file byte[] pdfBytes = Files.readAllBytes(tempOutputFile); @@ -55,6 +69,7 @@ public class ConvertOfficeController { return pdfBytes; } + private boolean isValidFileExtension(String fileExtension) { String extensionPattern = "^(?i)[a-z0-9]{2,4}$"; return fileExtension.matches(extensionPattern); @@ -62,17 +77,19 @@ public class ConvertOfficeController { @PostMapping(consumes = "multipart/form-data", value = "/file/pdf") @Operation( - summary = "Convert a file to a PDF using LibreOffice", - description = "This endpoint converts a given file to a PDF using LibreOffice API Input:Any Output:PDF Type:SISO" - ) - public ResponseEntity processFileToPDF(@ModelAttribute GeneralFile request) - throws Exception { - MultipartFile inputFile = request.getFileInput(); + summary = "Convert a file to a PDF using LibreOffice", + description = + "This endpoint converts a given file to a PDF using LibreOffice API Input:Any Output:PDF Type:SISO") + public ResponseEntity processFileToPDF(@ModelAttribute GeneralFile request) + throws Exception { + MultipartFile inputFile = request.getFileInput(); // unused but can start server instance if startup time is to long // LibreOfficeListener.getInstance().start(); byte[] pdfByteArray = convertToPdf(inputFile); - return WebResponseUtils.bytesToWebResponse(pdfByteArray, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_convertedToPDF.pdf"); + return WebResponseUtils.bytesToWebResponse( + pdfByteArray, + inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_convertedToPDF.pdf"); } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java index 11279a27..74b292b5 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java @@ -11,6 +11,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest; import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest; @@ -22,51 +23,70 @@ import stirling.software.SPDF.utils.PDFToFile; @Tag(name = "Convert", description = "Convert APIs") public class ConvertPDFToOffice { - @PostMapping(consumes = "multipart/form-data", value = "/pdf/html") - @Operation(summary = "Convert PDF to HTML", description = "This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO") - public ResponseEntity processPdfToHTML(@ModelAttribute PDFFile request) - throws Exception { - MultipartFile inputFile = request.getFileInput(); - PDFToFile pdfToFile = new PDFToFile(); - return pdfToFile.processPdfToOfficeFormat(inputFile, "html", "writer_pdf_import"); - } + @PostMapping(consumes = "multipart/form-data", value = "/pdf/html") + @Operation( + summary = "Convert PDF to HTML", + description = + "This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO") + public ResponseEntity processPdfToHTML(@ModelAttribute PDFFile request) + throws Exception { + MultipartFile inputFile = request.getFileInput(); + PDFToFile pdfToFile = new PDFToFile(); + return pdfToFile.processPdfToOfficeFormat(inputFile, "html", "writer_pdf_import"); + } - @PostMapping(consumes = "multipart/form-data", value = "/pdf/presentation") - @Operation(summary = "Convert PDF to Presentation format", description = "This endpoint converts a given PDF file to a Presentation format. Input:PDF Output:PPT Type:SISO") - public ResponseEntity processPdfToPresentation(@ModelAttribute PdfToPresentationRequest request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String outputFormat = request.getOutputFormat(); - PDFToFile pdfToFile = new PDFToFile(); - return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import"); - } + @PostMapping(consumes = "multipart/form-data", value = "/pdf/presentation") + @Operation( + summary = "Convert PDF to Presentation format", + description = + "This endpoint converts a given PDF file to a Presentation format. Input:PDF Output:PPT Type:SISO") + public ResponseEntity processPdfToPresentation( + @ModelAttribute PdfToPresentationRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String outputFormat = request.getOutputFormat(); + PDFToFile pdfToFile = new PDFToFile(); + return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import"); + } - @PostMapping(consumes = "multipart/form-data", value = "/pdf/text") - @Operation(summary = "Convert PDF to Text or RTF format", description = "This endpoint converts a given PDF file to Text or RTF format. Input:PDF Output:TXT Type:SISO") - public ResponseEntity processPdfToRTForTXT(@ModelAttribute PdfToTextOrRTFRequest request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String outputFormat = request.getOutputFormat(); + @PostMapping(consumes = "multipart/form-data", value = "/pdf/text") + @Operation( + summary = "Convert PDF to Text or RTF format", + description = + "This endpoint converts a given PDF file to Text or RTF format. Input:PDF Output:TXT Type:SISO") + public ResponseEntity processPdfToRTForTXT( + @ModelAttribute PdfToTextOrRTFRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String outputFormat = request.getOutputFormat(); - PDFToFile pdfToFile = new PDFToFile(); - return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); - } + PDFToFile pdfToFile = new PDFToFile(); + return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); + } - @PostMapping(consumes = "multipart/form-data", value = "/pdf/word") - @Operation(summary = "Convert PDF to Word document", description = "This endpoint converts a given PDF file to a Word document format. Input:PDF Output:WORD Type:SISO") - public ResponseEntity processPdfToWord(@ModelAttribute PdfToWordRequest request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String outputFormat = request.getOutputFormat(); - PDFToFile pdfToFile = new PDFToFile(); - return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); - } + @PostMapping(consumes = "multipart/form-data", value = "/pdf/word") + @Operation( + summary = "Convert PDF to Word document", + description = + "This endpoint converts a given PDF file to a Word document format. Input:PDF Output:WORD Type:SISO") + public ResponseEntity processPdfToWord(@ModelAttribute PdfToWordRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String outputFormat = request.getOutputFormat(); + PDFToFile pdfToFile = new PDFToFile(); + return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); + } - @PostMapping(consumes = "multipart/form-data", value = "/pdf/xml") - @Operation(summary = "Convert PDF to XML", description = "This endpoint converts a PDF file to an XML file. Input:PDF Output:XML Type:SISO") - public ResponseEntity processPdfToXML(@ModelAttribute PDFFile request) - throws Exception { - MultipartFile inputFile = request.getFileInput(); - - PDFToFile pdfToFile = new PDFToFile(); - return pdfToFile.processPdfToOfficeFormat(inputFile, "xml", "writer_pdf_import"); - } + @PostMapping(consumes = "multipart/form-data", value = "/pdf/xml") + @Operation( + summary = "Convert PDF to XML", + description = + "This endpoint converts a PDF file to an XML file. Input:PDF Output:XML Type:SISO") + public ResponseEntity processPdfToXML(@ModelAttribute PDFFile request) + throws Exception { + MultipartFile inputFile = request.getFileInput(); + PDFToFile pdfToFile = new PDFToFile(); + return pdfToFile.processPdfToOfficeFormat(inputFile, "xml", "writer_pdf_import"); + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java index 32ccb84a..ac8ce031 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java @@ -14,6 +14,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; @@ -24,14 +25,13 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "Convert", description = "Convert APIs") public class ConvertPDFToPDFA { - @PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa") - @Operation( - summary = "Convert a PDF to a PDF/A", - description = "This endpoint converts a PDF file to a PDF/A file. PDF/A is a format designed for long-term archiving of digital documents. Input:PDF Output:PDF Type:SISO" - ) - public ResponseEntity pdfToPdfA(@ModelAttribute PDFFile request) - throws Exception { - MultipartFile inputFile = request.getFileInput(); + @PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa") + @Operation( + summary = "Convert a PDF to a PDF/A", + description = + "This endpoint converts a PDF file to a PDF/A file. PDF/A is a format designed for long-term archiving of digital documents. Input:PDF Output:PDF Type:SISO") + public ResponseEntity pdfToPdfA(@ModelAttribute PDFFile request) throws Exception { + MultipartFile inputFile = request.getFileInput(); // Save the uploaded file to a temporary location Path tempInputFile = Files.createTempFile("input_", ".pdf"); @@ -50,7 +50,9 @@ public class ConvertPDFToPDFA { command.add(tempInputFile.toString()); command.add(tempOutputFile.toString()); - ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command); + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF) + .runCommandWithOutputHandling(command); // Read the optimized PDF file byte[] pdfBytes = Files.readAllBytes(tempOutputFile); @@ -60,8 +62,8 @@ public class ConvertPDFToPDFA { Files.delete(tempOutputFile); // Return the optimized PDF as a response - String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_PDFA.pdf"; + String outputFilename = + inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_PDFA.pdf"; return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java index d3f5c307..bf631c87 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java @@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.converters.UrlToPdfRequest; import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.ProcessExecutor; @@ -25,52 +26,52 @@ import stirling.software.SPDF.utils.WebResponseUtils; @RequestMapping("/api/v1/convert") public class ConvertWebsiteToPDF { - @PostMapping(consumes = "multipart/form-data", value = "/url/pdf") - @Operation( - summary = "Convert a URL to a PDF", - description = "This endpoint fetches content from a URL and converts it to a PDF format. Input:N/A Output:PDF Type:SISO" - ) - public ResponseEntity urlToPdf(@ModelAttribute UrlToPdfRequest request) throws IOException, InterruptedException { - String URL = request.getUrlInput(); + @PostMapping(consumes = "multipart/form-data", value = "/url/pdf") + @Operation( + summary = "Convert a URL to a PDF", + description = + "This endpoint fetches content from a URL and converts it to a PDF format. Input:N/A Output:PDF Type:SISO") + public ResponseEntity urlToPdf(@ModelAttribute UrlToPdfRequest request) + throws IOException, InterruptedException { + String URL = request.getUrlInput(); - // Validate the URL format - if(!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) { - throw new IllegalArgumentException("Invalid URL format provided."); - } - Path tempOutputFile = null; - byte[] pdfBytes; - try { - // Prepare the output file path - tempOutputFile = Files.createTempFile("output_", ".pdf"); - - // Prepare the OCRmyPDF command - List command = new ArrayList<>(); - command.add("weasyprint"); - command.add(URL); - command.add(tempOutputFile.toString()); - - ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT).runCommandWithOutputHandling(command); - - // Read the optimized PDF file - pdfBytes = Files.readAllBytes(tempOutputFile); - } - finally { - // Clean up the temporary files - Files.delete(tempOutputFile); - } - // Convert URL to a safe filename - String outputFilename = convertURLToFileName(URL); - - return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); - } + // Validate the URL format + if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) { + throw new IllegalArgumentException("Invalid URL format provided."); + } + Path tempOutputFile = null; + byte[] pdfBytes; + try { + // Prepare the output file path + tempOutputFile = Files.createTempFile("output_", ".pdf"); - private String convertURLToFileName(String url) { - String safeName = url.replaceAll("[^a-zA-Z0-9]", "_"); - if(safeName.length() > 50) { - safeName = safeName.substring(0, 50); // restrict to 50 characters - } - return safeName + ".pdf"; - } + // Prepare the OCRmyPDF command + List command = new ArrayList<>(); + command.add("weasyprint"); + command.add(URL); + command.add(tempOutputFile.toString()); + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT) + .runCommandWithOutputHandling(command); + // Read the optimized PDF file + pdfBytes = Files.readAllBytes(tempOutputFile); + } finally { + // Clean up the temporary files + Files.delete(tempOutputFile); + } + // Convert URL to a safe filename + String outputFilename = convertURLToFileName(URL); + + return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); + } + + private String convertURLToFileName(String url) { + String safeName = url.replaceAll("[^a-zA-Z0-9]", "_"); + if (safeName.length() > 50) { + safeName = safeName.substring(0, 50); // restrict to 50 characters + } + return safeName + ".pdf"; + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractController.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractController.java index 6398e8b9..c5b9ea8d 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractController.java @@ -22,6 +22,7 @@ import com.opencsv.CSVWriter; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.controller.api.CropController; import stirling.software.SPDF.controller.api.strippers.PDFTableStripper; import stirling.software.SPDF.model.api.extract.PDFFilePage; @@ -34,21 +35,24 @@ public class ExtractController { private static final Logger logger = LoggerFactory.getLogger(CropController.class); @PostMapping(value = "/pdf/csv", consumes = "multipart/form-data") - @Operation(summary = "Extracts a PDF document to csv", description = "This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO") - public ResponseEntity PdfToCsv(@ModelAttribute PDFFilePage form) - throws Exception { + @Operation( + summary = "Extracts a PDF document to csv", + description = + "This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO") + public ResponseEntity PdfToCsv(@ModelAttribute PDFFilePage form) throws Exception { ArrayList tableData = new ArrayList<>(); int columnsCount = 0; - try (PDDocument document = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) { + try (PDDocument document = + PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) { final double res = 72; // PDF units are at 72 DPI PDFTableStripper stripper = new PDFTableStripper(); PDPage pdPage = document.getPage(form.getPageId() - 1); stripper.extractTable(pdPage); columnsCount = stripper.getColumns(); for (int c = 0; c < columnsCount; ++c) { - for(int r=0; r notEmptyColumns = new ArrayList<>(); - for (String item: tableData) { - if(!item.trim().isEmpty()){ + for (String item : tableData) { + if (!item.trim().isEmpty()) { notEmptyColumns.add(item); - }else{ + } else { columnsCount--; } } - List fullTable = notEmptyColumns.stream().map((entity)-> - entity.replace('\n',' ').replace('\r',' ').trim().replaceAll("\\s{2,}", "|")).toList(); + List fullTable = + notEmptyColumns.stream() + .map( + (entity) -> + entity.replace('\n', ' ') + .replace('\r', ' ') + .trim() + .replaceAll("\\s{2,}", "|")) + .toList(); int rowsCount = fullTable.get(0).split("\\|").length; - ArrayList headersList = getTableHeaders(columnsCount,fullTable); - ArrayList recordList = getRecordsList(rowsCount,fullTable); - - if(headersList.size() == 0 && recordList.size() == 0) { - throw new Exception("No table detected, no headers or records found"); + ArrayList headersList = getTableHeaders(columnsCount, fullTable); + ArrayList recordList = getRecordsList(rowsCount, fullTable); + + if (headersList.size() == 0 && recordList.size() == 0) { + throw new Exception("No table detected, no headers or records found"); } - + StringWriter writer = new StringWriter(); try (CSVWriter csvWriter = new CSVWriter(writer)) { csvWriter.writeNext(headersList.toArray(new String[0])); @@ -85,35 +96,41 @@ public class ExtractController { } HttpHeaders headers = new HttpHeaders(); - headers.setContentDisposition(ContentDisposition.builder("attachment").filename(form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_extracted.csv").build()); + headers.setContentDisposition( + ContentDisposition.builder("attachment") + .filename( + form.getFileInput() + .getOriginalFilename() + .replaceFirst("[.][^.]+$", "") + + "_extracted.csv") + .build()); headers.setContentType(MediaType.parseMediaType("text/csv")); - return ResponseEntity.ok() - .headers(headers) - .body(writer.toString()); + return ResponseEntity.ok().headers(headers).body(writer.toString()); } - private ArrayList getRecordsList( int rowsCounts ,List items){ + private ArrayList getRecordsList(int rowsCounts, List items) { ArrayList recordsList = new ArrayList<>(); - for (int b=1; b getTableHeaders(int columnsCount, List items){ + + private ArrayList getTableHeaders(int columnsCount, List items) { ArrayList resultList = new ArrayList<>(); - for (int i=0;i containsText(@ModelAttribute ContainsTextRequest request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String text = request.getText(); - String pageNumber = request.getPageNumbers(); - - PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); - if (PdfUtils.hasText(pdfDocument, pageNumber, text)) - return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename()); - return null; - } + @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(@ModelAttribute ContainsTextRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String text = request.getText(); + String pageNumber = request.getPageNumbers(); - // 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(@ModelAttribute PDFWithPageNums request) - throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String pageNumber = request.getPageNumbers(); - - PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); - if (PdfUtils.hasImages(pdfDocument, pageNumber)) - return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename()); - return null; - } + PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); + if (PdfUtils.hasText(pdfDocument, pageNumber, text)) + 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(@ModelAttribute PDFComparisonAndCount request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String pageCount = request.getPageCount(); - String comparator = request.getComparator(); - // Load the PDF - PDDocument document = PDDocument.load(inputFile.getInputStream()); - int actualPageCount = document.getNumberOfPages(); + // 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(@ModelAttribute PDFWithPageNums request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String pageNumber = request.getPageNumbers(); - 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); - } + PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); + if (PdfUtils.hasImages(pdfDocument, pageNumber)) + return WebResponseUtils.pdfDocToWebResponse( + pdfDocument, inputFile.getOriginalFilename()); + return null; + } - if (valid) - return WebResponseUtils.multiPartFileToWebResponse(inputFile); - 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(@ModelAttribute PDFComparisonAndCount request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String pageCount = request.getPageCount(); + String comparator = request.getComparator(); + // Load the PDF + PDDocument document = PDDocument.load(inputFile.getInputStream()); + int actualPageCount = document.getNumberOfPages(); - @PostMapping(consumes = "multipart/form-data", value = "/filter-page-size") - @Operation(summary = "Checks if a PDF is of a certain size", description = "Input:PDF Output:Boolean Type:SISO") - public ResponseEntity pageSize(@ModelAttribute PageSizeRequest request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String standardPageSize = request.getStandardPageSize(); - String comparator = request.getComparator(); + 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); + } - // Load the PDF - PDDocument document = PDDocument.load(inputFile.getInputStream()); + if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); + return null; + } - PDPage firstPage = document.getPage(0); - PDRectangle actualPageSize = firstPage.getMediaBox(); + @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(@ModelAttribute PageSizeRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String standardPageSize = request.getStandardPageSize(); + String comparator = request.getComparator(); - // Calculate the area of the actual page size - float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight(); + // Load the PDF + PDDocument document = PDDocument.load(inputFile.getInputStream()); - // Get the standard size and calculate its area - PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize); - float standardArea = standardSize.getWidth() * standardSize.getHeight(); + PDPage firstPage = document.getPage(0); + PDRectangle actualPageSize = firstPage.getMediaBox(); - 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); - } + // Calculate the area of the actual page size + float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight(); - if (valid) - return WebResponseUtils.multiPartFileToWebResponse(inputFile); - return null; - } + // Get the standard size and calculate its area + PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize); + float standardArea = standardSize.getWidth() * standardSize.getHeight(); - @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(@ModelAttribute FileSizeRequest request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String fileSize = request.getFileSize(); - String comparator = request.getComparator(); + 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); + } - // Get the file size - long actualFileSize = inputFile.getSize(); + if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); + return null; + } - 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); - } + @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(@ModelAttribute FileSizeRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String fileSize = request.getFileSize(); + String comparator = request.getComparator(); - if (valid) - return WebResponseUtils.multiPartFileToWebResponse(inputFile); - return null; - } + // Get the file size + long actualFileSize = inputFile.getSize(); - @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(@ModelAttribute PageRotationRequest request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - int rotation = request.getRotation(); - String comparator = request.getComparator(); + 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); + } - // Load the PDF - PDDocument document = PDDocument.load(inputFile.getInputStream()); + if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); + return null; + } - // Get the rotation of the first page - PDPage firstPage = document.getPage(0); - int actualRotation = firstPage.getRotation(); - boolean valid = false; - // Perform the comparison - switch (comparator) { - case "Greater": - valid = actualRotation > rotation; - break; - case "Equal": - valid = actualRotation == rotation; - break; - case "Less": - valid = actualRotation < rotation; - break; - default: - throw new IllegalArgumentException("Invalid comparator: " + comparator); - } + @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(@ModelAttribute PageRotationRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + int rotation = request.getRotation(); + String comparator = request.getComparator(); - if (valid) - return WebResponseUtils.multiPartFileToWebResponse(inputFile); - return null; + // 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/misc/AutoRenameController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java index fe8337d2..e81ef1e1 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java @@ -19,8 +19,10 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/misc") @Tag(name = "Misc", description = "Miscellaneous APIs") @@ -32,97 +34,105 @@ public class AutoRenameController { private static final int LINE_LIMIT = 11; @PostMapping(consumes = "multipart/form-data", value = "/auto-rename") - @Operation(summary = "Extract header from PDF file", description = "This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO") - public ResponseEntity extractHeader(@ModelAttribute ExtractHeaderRequest request) throws Exception { + @Operation( + summary = "Extract header from PDF file", + description = + "This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO") + public ResponseEntity extractHeader(@ModelAttribute ExtractHeaderRequest request) + throws Exception { MultipartFile file = request.getFileInput(); Boolean useFirstTextAsFallback = request.isUseFirstTextAsFallback(); - PDDocument document = PDDocument.load(file.getInputStream()); - PDFTextStripper reader = new PDFTextStripper() { - class LineInfo { - String text; - float fontSize; + 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; - } - } + 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; + 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(); - } - } - } + @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)); - } - } + 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 + @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)); - } + // 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; + // 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); - } + return title != null + ? title + : (useFirstTextAsFallback + ? (mergedLineInfos.isEmpty() + ? null + : mergedLineInfos.get(mergedLineInfos.size() - 1) + .text) + : null); + } + }; - }; + String header = reader.getText(document); - String header = reader.getText(document); - - - // Sanitize the header string by removing characters not allowed in a filename. 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()); + 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/misc/AutoSplitPdfController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java index e62fc35f..e32e325e 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.controller.api.misc; + import java.awt.image.BufferedImage; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferInt; @@ -32,6 +33,7 @@ import com.google.zxing.common.HybridBinarizer; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest; import stirling.software.SPDF.utils.WebResponseUtils; @@ -43,8 +45,12 @@ 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-PDF Type:SISO") - public ResponseEntity autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request) throws IOException { + @Operation( + summary = "Auto split PDF pages into separate documents", + description = + "This endpoint accepts a PDF file, scans each page for a specific QR code, and splits the document at the QR code boundaries. The output is a zip file containing each separate PDF document. Input:PDF Output:ZIP-PDF Type:SISO") + public ResponseEntity autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request) + throws IOException { MultipartFile file = request.getFileInput(); boolean duplexMode = request.isDuplexMode(); @@ -107,29 +113,48 @@ public class AutoSplitPdfController { } catch (Exception e) { e.printStackTrace(); } finally { - data = Files.readAllBytes(zipFile); + data = Files.readAllBytes(zipFile); Files.delete(zipFile); } - return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.bytesToWebResponse( + data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); } - private static String decodeQRCode(BufferedImage bufferedImage) { LuminanceSource source; if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte) { byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData(); - source = new PlanarYUVLuminanceSource(pixels, bufferedImage.getWidth(), bufferedImage.getHeight(), 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight(), false); + source = + new PlanarYUVLuminanceSource( + pixels, + bufferedImage.getWidth(), + bufferedImage.getHeight(), + 0, + 0, + bufferedImage.getWidth(), + bufferedImage.getHeight(), + false); } else if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferInt) { int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData(); byte[] newPixels = new byte[pixels.length]; for (int i = 0; i < pixels.length; i++) { newPixels[i] = (byte) (pixels[i] & 0xff); } - source = new PlanarYUVLuminanceSource(newPixels, bufferedImage.getWidth(), bufferedImage.getHeight(), 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight(), false); + source = + new PlanarYUVLuminanceSource( + newPixels, + bufferedImage.getWidth(), + bufferedImage.getHeight(), + 0, + 0, + bufferedImage.getWidth(), + bufferedImage.getHeight(), + false); } else { - throw new IllegalArgumentException("BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed int), byte gray, or 3-byte/4-byte RGB image data"); + throw new IllegalArgumentException( + "BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed int), byte gray, or 3-byte/4-byte RGB image data"); } BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java index c52ff61a..036d6a66 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java @@ -28,6 +28,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest; import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.ProcessExecutor; @@ -39,17 +40,18 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "Misc", description = "Miscellaneous APIs") public class BlankPageController { - @PostMapping(consumes = "multipart/form-data", value = "/remove-blanks") - @Operation( - summary = "Remove blank pages from a PDF file", - description = "This endpoint removes blank pages from a given PDF file. Users can specify the threshold and white percentage to tune the detection of blank pages. Input:PDF Output:PDF Type:SISO" - ) - public ResponseEntity removeBlankPages(@ModelAttribute RemoveBlankPagesRequest request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - int threshold = request.getThreshold(); - float whitePercent = request.getWhitePercent(); - - PDDocument document = null; + @PostMapping(consumes = "multipart/form-data", value = "/remove-blanks") + @Operation( + summary = "Remove blank pages from a PDF file", + description = + "This endpoint removes blank pages from a given PDF file. Users can specify the threshold and white percentage to tune the detection of blank pages. Input:PDF Output:PDF Type:SISO") + public ResponseEntity removeBlankPages(@ModelAttribute RemoveBlankPagesRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + int threshold = request.getThreshold(); + float whitePercent = request.getWhitePercent(); + + PDDocument document = null; try { document = PDDocument.load(inputFile.getInputStream()); PDPageTree pages = document.getDocumentCatalog().getPages(); @@ -72,21 +74,34 @@ public class BlankPageController { boolean hasImages = PdfUtils.hasImagesOnPage(page); if (hasImages) { System.out.println("page " + pageIndex + " has image"); - + Path tempFile = Files.createTempFile("image_", ".png"); - + // Render image and save as temp file BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 300); ImageIO.write(image, "png", tempFile.toFile()); - - List command = new ArrayList<>(Arrays.asList("python3", System.getProperty("user.dir") + "/scripts/detect-blank-pages.py", tempFile.toString() ,"--threshold", String.valueOf(threshold), "--white_percent", String.valueOf(whitePercent))); - + + List command = + new ArrayList<>( + Arrays.asList( + "python3", + System.getProperty("user.dir") + + "/scripts/detect-blank-pages.py", + tempFile.toString(), + "--threshold", + String.valueOf(threshold), + "--white_percent", + String.valueOf(whitePercent))); + // Run CLI command - ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV).runCommandWithOutputHandling(command); - + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV) + .runCommandWithOutputHandling(command); + // does contain data if (returnCode.getRc() == 0) { - System.out.println("page " + pageIndex + " has image which is not blank"); + System.out.println( + "page " + pageIndex + " has image which is not blank"); pagesToKeepIndex.add(pageIndex); } else { System.out.println("Skipping, Image was blank for page #" + pageIndex); @@ -94,12 +109,12 @@ public class BlankPageController { } } pageIndex++; - } System.out.print("pagesToKeep=" + pagesToKeepIndex.size()); // Remove pages not present in pagesToKeepIndex - List pageIndices = IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList()); + List pageIndices = + IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList()); Collections.reverse(pageIndices); // Reverse to prevent index shifting during removal for (Integer i : pageIndices) { if (!pagesToKeepIndex.contains(i)) { @@ -107,16 +122,15 @@ public class BlankPageController { } } - return WebResponseUtils.pdfDocToWebResponse(document, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_blanksRemoved.pdf"); + return WebResponseUtils.pdfDocToWebResponse( + document, + inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_blanksRemoved.pdf"); } catch (IOException e) { e.printStackTrace(); return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } finally { - if (document != null) - document.close(); + if (document != null) document.close(); } } - - - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java index dd864bc1..fd9a0460 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java @@ -30,6 +30,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.misc.OptimizePdfRequest; import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.ProcessExecutor; @@ -44,20 +45,23 @@ public class CompressController { private static final Logger logger = LoggerFactory.getLogger(CompressController.class); @PostMapping(consumes = "multipart/form-data", value = "/compress-pdf") - @Operation(summary = "Optimize PDF file", description = "This endpoint accepts a PDF file and optimizes it based on the provided parameters. Input:PDF Output:PDF Type:SISO") - public ResponseEntity optimizePdf(@ModelAttribute OptimizePdfRequest request) throws Exception { + @Operation( + summary = "Optimize PDF file", + description = + "This endpoint accepts a PDF file and optimizes it based on the provided parameters. Input:PDF Output:PDF Type:SISO") + public ResponseEntity optimizePdf(@ModelAttribute OptimizePdfRequest request) + throws Exception { MultipartFile inputFile = request.getFileInput(); Integer optimizeLevel = request.getOptimizeLevel(); String expectedOutputSizeString = request.getExpectedOutputSize(); - - if(expectedOutputSizeString == null && optimizeLevel == null) { + if (expectedOutputSizeString == null && optimizeLevel == null) { throw new Exception("Both expected output size and optimize level are not specified"); } Long expectedOutputSize = 0L; boolean autoMode = false; - if (expectedOutputSizeString != null && expectedOutputSizeString.length() > 1 ) { + if (expectedOutputSizeString != null && expectedOutputSizeString.length() > 1) { expectedOutputSize = GeneralUtils.convertSizeToBytes(expectedOutputSizeString); autoMode = true; } @@ -71,8 +75,9 @@ public class CompressController { // Prepare the output file path Path tempOutputFile = Files.createTempFile("output_", ".pdf"); - // Determine initial optimization level based on expected size reduction, only if in autoMode - if(autoMode) { + // Determine initial optimization level based on expected size reduction, only if in + // autoMode + if (autoMode) { double sizeReductionRatio = expectedOutputSize / (double) inputFileSize; if (sizeReductionRatio > 0.7) { optimizeLevel = 1; @@ -94,20 +99,20 @@ public class CompressController { command.add("-dCompatibilityLevel=1.4"); switch (optimizeLevel) { - case 1: - command.add("-dPDFSETTINGS=/prepress"); - break; - case 2: - command.add("-dPDFSETTINGS=/printer"); - break; - case 3: - command.add("-dPDFSETTINGS=/ebook"); - break; - case 4: - command.add("-dPDFSETTINGS=/screen"); - break; - default: - command.add("-dPDFSETTINGS=/default"); + case 1: + command.add("-dPDFSETTINGS=/prepress"); + break; + case 2: + command.add("-dPDFSETTINGS=/printer"); + break; + case 3: + command.add("-dPDFSETTINGS=/ebook"); + break; + case 4: + command.add("-dPDFSETTINGS=/screen"); + break; + default: + command.add("-dPDFSETTINGS=/default"); } command.add("-dNOPAUSE"); @@ -116,7 +121,9 @@ public class CompressController { command.add("-sOutputFile=" + tempOutputFile.toString()); command.add(tempInputFile.toString()); - ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(command); + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT) + .runCommandWithOutputHandling(command); // Check if file size is within expected size or not auto mode so instantly finish long outputFileSize = Files.size(tempOutputFile); @@ -125,19 +132,18 @@ public class CompressController { } else { // Increase optimization level for next iteration optimizeLevel++; - if(autoMode && optimizeLevel > 3) { + if (autoMode && optimizeLevel > 3) { System.out.println("Skipping level 4 due to bad results in auto mode"); sizeMet = true; - } else if(optimizeLevel == 5) { - + } else if (optimizeLevel == 5) { + } else { - System.out.println("Increasing ghostscript optimisation level to " + optimizeLevel); + System.out.println( + "Increasing ghostscript optimisation level to " + optimizeLevel); } } } - - if (expectedOutputSize != null && autoMode) { long outputFileSize = Files.size(tempOutputFile); if (outputFileSize > expectedOutputSize) { @@ -157,8 +163,8 @@ public class CompressController { BufferedImage bufferedImage = image.getImage(); // Calculate the new dimensions - int newWidth = (int)(bufferedImage.getWidth() * scaleFactor); - int newHeight = (int)(bufferedImage.getHeight() * scaleFactor); + int newWidth = (int) (bufferedImage.getWidth() * scaleFactor); + int newHeight = (int) (bufferedImage.getHeight() * scaleFactor); // If the new dimensions are zero, skip this iteration if (newWidth == 0 || newHeight == 0) { @@ -166,23 +172,39 @@ public class CompressController { } // Otherwise, proceed with the scaling - Image scaledImage = bufferedImage.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH); + Image scaledImage = + bufferedImage.getScaledInstance( + newWidth, newHeight, Image.SCALE_SMOOTH); // Convert the scaled image back to a BufferedImage - BufferedImage scaledBufferedImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB); - scaledBufferedImage.getGraphics().drawImage(scaledImage, 0, 0, null); + BufferedImage scaledBufferedImage = + new BufferedImage( + newWidth, + newHeight, + BufferedImage.TYPE_INT_RGB); + scaledBufferedImage + .getGraphics() + .drawImage(scaledImage, 0, 0, null); // Compress the scaled image - ByteArrayOutputStream compressedImageStream = new ByteArrayOutputStream(); - ImageIO.write(scaledBufferedImage, "jpeg", compressedImageStream); + ByteArrayOutputStream compressedImageStream = + new ByteArrayOutputStream(); + ImageIO.write( + scaledBufferedImage, "jpeg", compressedImageStream); byte[] imageBytes = compressedImageStream.toByteArray(); compressedImageStream.close(); // Convert compressed image back to PDImageXObject - ByteArrayInputStream bais = new ByteArrayInputStream(imageBytes); - PDImageXObject compressedImage = PDImageXObject.createFromByteArray(doc, imageBytes, image.getCOSObject().toString()); + ByteArrayInputStream bais = + new ByteArrayInputStream(imageBytes); + PDImageXObject compressedImage = + PDImageXObject.createFromByteArray( + doc, + imageBytes, + image.getCOSObject().toString()); - // Replace the image in the resources with the compressed version + // Replace the image in the resources with the compressed + // version res.put(name, compressedImage); } } @@ -194,16 +216,23 @@ public class CompressController { long currentSize = Files.size(tempOutputFile); // Check if the overall PDF size is still larger than expectedOutputSize if (currentSize > expectedOutputSize) { - // Log the current file size and scaleFactor - - System.out.println("Current file size: " + FileUtils.byteCountToDisplaySize(currentSize)); + // Log the current file size and scaleFactor + + System.out.println( + "Current file size: " + + FileUtils.byteCountToDisplaySize(currentSize)); System.out.println("Current scale factor: " + scaleFactor); // The file is still too large, reduce scaleFactor and try again scaleFactor *= 0.9; // reduce scaleFactor by 10% // Avoid scaleFactor being too small, causing the image to shrink to 0 - if(scaleFactor < 0.2 || previousFileSize == currentSize){ - throw new RuntimeException("Could not reach the desired size without excessively degrading image quality, lowest size recommended is " + FileUtils.byteCountToDisplaySize(currentSize) + ", " + currentSize + " bytes"); + if (scaleFactor < 0.2 || previousFileSize == currentSize) { + throw new RuntimeException( + "Could not reach the desired size without excessively degrading image quality, lowest size recommended is " + + FileUtils.byteCountToDisplaySize(currentSize) + + ", " + + currentSize + + " bytes"); } previousFileSize = currentSize; } else { @@ -211,10 +240,7 @@ public class CompressController { break; } } - } - - } } @@ -222,9 +248,10 @@ public class CompressController { byte[] pdfBytes = Files.readAllBytes(tempOutputFile); // Check if optimized file is larger than the original - if(pdfBytes.length > inputFileSize) { + if (pdfBytes.length > inputFileSize) { // Log the occurrence - logger.warn("Optimized file is larger than the original. Returning the original file instead."); + logger.warn( + "Optimized file is larger than the original. Returning the original file instead."); // Read the original file again pdfBytes = Files.readAllBytes(tempInputFile); @@ -235,8 +262,8 @@ public class CompressController { Files.delete(tempOutputFile); // Return the optimized PDF as a response - String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_Optimized.pdf"; + String outputFilename = + inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_Optimized.pdf"; return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java index d5906970..257f4d52 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java @@ -32,10 +32,12 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest; import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/misc") @Tag(name = "Misc", description = "Miscellaneous APIs") @@ -44,18 +46,28 @@ public class ExtractImageScansController { private static final Logger logger = LoggerFactory.getLogger(ExtractImageScansController.class); @PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans") - @Operation(summary = "Extract image scans from an input file", - description = "This endpoint extracts image scans from a given file based on certain parameters. Users can specify angle threshold, tolerance, minimum area, minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP Type:SIMO") + @Operation( + summary = "Extract image scans from an input file", + description = + "This endpoint extracts image scans from a given file based on certain parameters. Users can specify angle threshold, tolerance, minimum area, minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP Type:SIMO") public ResponseEntity extractImageScans( - @RequestBody( - description = "Form data containing file and extraction parameters", - required = true, - content = @Content( - mediaType = "multipart/form-data", - schema = @Schema(implementation = ExtractImageScansRequest.class) // This should represent your form's structure - ) - ) - ExtractImageScansRequest form) throws IOException, InterruptedException { + @RequestBody( + description = "Form data containing file and extraction parameters", + required = true, + content = + @Content( + mediaType = "multipart/form-data", + schema = + @Schema( + implementation = + ExtractImageScansRequest + .class) // This should + // represent + // your form's + // structure + )) + ExtractImageScansRequest form) + throws IOException, InterruptedException { String fileName = form.getFileInput().getOriginalFilename(); String extension = fileName.substring(fileName.lastIndexOf(".") + 1); @@ -64,7 +76,8 @@ public class ExtractImageScansController { // Check if input file is a PDF if (extension.equalsIgnoreCase("pdf")) { // Load PDF document - try (PDDocument document = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) { + try (PDDocument document = + PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) { PDFRenderer pdfRenderer = new PDFRenderer(document); int pageCount = document.getNumberOfPages(); images = new ArrayList<>(); @@ -84,7 +97,10 @@ public class ExtractImageScansController { } } else { Path tempInputFile = Files.createTempFile("input_", "." + extension); - Files.copy(form.getFileInput().getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING); + Files.copy( + form.getFileInput().getInputStream(), + tempInputFile, + StandardCopyOption.REPLACE_EXISTING); // Add input file path to images list images.add(tempInputFile.toString()); } @@ -95,21 +111,28 @@ public class ExtractImageScansController { for (int i = 0; i < images.size(); i++) { Path tempDir = Files.createTempDirectory("openCV_output"); - List command = new ArrayList<>(Arrays.asList( - "python3", - "./scripts/split_photos.py", - images.get(i), - tempDir.toString(), - "--angle_threshold", String.valueOf(form.getAngleThreshold()), - "--tolerance", String.valueOf(form.getTolerance()), - "--min_area", String.valueOf(form.getMinArea()), - "--min_contour_area", String.valueOf(form.getMinContourArea()), - "--border_size", String.valueOf(form.getBorderSize()) - )); - + List command = + new ArrayList<>( + Arrays.asList( + "python3", + "./scripts/split_photos.py", + images.get(i), + tempDir.toString(), + "--angle_threshold", + String.valueOf(form.getAngleThreshold()), + "--tolerance", + String.valueOf(form.getTolerance()), + "--min_area", + String.valueOf(form.getMinArea()), + "--min_contour_area", + String.valueOf(form.getMinContourArea()), + "--border_size", + String.valueOf(form.getBorderSize()))); // Run CLI command - ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV).runCommandWithOutputHandling(command); + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV) + .runCommandWithOutputHandling(command); // Read the output photos in temp directory List tempOutputFiles = Files.list(tempDir).sorted().collect(Collectors.toList()); @@ -126,10 +149,16 @@ public class ExtractImageScansController { String outputZipFilename = fileName.replaceFirst("[.][^.]+$", "") + "_processed.zip"; Path tempZipFile = Files.createTempFile("output_", ".zip"); - try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) { + try (ZipOutputStream zipOut = + new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) { // Add processed images to the zip for (int i = 0; i < processedImageBytes.size(); i++) { - ZipEntry entry = new ZipEntry(fileName.replaceFirst("[.][^.]+$", "") + "_" + (i + 1) + ".png"); + ZipEntry entry = + new ZipEntry( + fileName.replaceFirst("[.][^.]+$", "") + + "_" + + (i + 1) + + ".png"); zipOut.putNextEntry(entry); zipOut.write(processedImageBytes.get(i)); zipOut.closeEntry(); @@ -141,13 +170,15 @@ public class ExtractImageScansController { // Clean up the temporary zip file Files.delete(tempZipFile); - return WebResponseUtils.bytesToWebResponse(zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.bytesToWebResponse( + zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); } else { // Return the processed image as a response byte[] imageBytes = processedImageBytes.get(0); - return WebResponseUtils.bytesToWebResponse(imageBytes, fileName.replaceFirst("[.][^.]+$", "") + ".png", MediaType.IMAGE_PNG); + return WebResponseUtils.bytesToWebResponse( + imageBytes, + fileName.replaceFirst("[.][^.]+$", "") + ".png", + MediaType.IMAGE_PNG); } - } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java index 6e18f1f2..f436d9f6 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.controller.api.misc; + import java.awt.Graphics2D; import java.awt.Image; import java.awt.image.BufferedImage; @@ -29,8 +30,10 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.PDFWithImageFormatRequest; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/misc") @Tag(name = "Misc", description = "Miscellaneous APIs") @@ -39,13 +42,17 @@ public class ExtractImagesController { private static final Logger logger = LoggerFactory.getLogger(ExtractImagesController.class); @PostMapping(consumes = "multipart/form-data", value = "/extract-images") - @Operation(summary = "Extract images from a PDF file", - description = "This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input:PDF Output:IMAGE/ZIP Type:SIMO") - public ResponseEntity extractImages(@ModelAttribute PDFWithImageFormatRequest request) throws IOException { + @Operation( + summary = "Extract images from a PDF file", + description = + "This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input:PDF Output:IMAGE/ZIP Type:SIMO") + public ResponseEntity extractImages(@ModelAttribute PDFWithImageFormatRequest request) + throws IOException { MultipartFile file = request.getFileInput(); String format = request.getFormat(); - System.out.println(System.currentTimeMillis() + "file=" + file.getName() + ", format=" + format); + System.out.println( + System.currentTimeMillis() + "file=" + file.getName() + ", format=" + format); PDDocument document = PDDocument.load(file.getBytes()); // Create ByteArrayOutputStream to write zip file to byte array @@ -69,24 +76,37 @@ public class ExtractImagesController { if (page.getResources().isImageXObject(name)) { PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name); int imageHash = image.hashCode(); - if(processedImages.contains(imageHash)) { + if (processedImages.contains(imageHash)) { continue; // Skip already processed images } processedImages.add(imageHash); - + // Convert image to desired format RenderedImage renderedImage = image.getImage(); BufferedImage bufferedImage = null; if (format.equalsIgnoreCase("png")) { - bufferedImage = new BufferedImage(renderedImage.getWidth(), renderedImage.getHeight(), BufferedImage.TYPE_INT_ARGB); + bufferedImage = + new BufferedImage( + renderedImage.getWidth(), + renderedImage.getHeight(), + BufferedImage.TYPE_INT_ARGB); } else if (format.equalsIgnoreCase("jpeg") || format.equalsIgnoreCase("jpg")) { - bufferedImage = new BufferedImage(renderedImage.getWidth(), renderedImage.getHeight(), BufferedImage.TYPE_INT_RGB); + bufferedImage = + new BufferedImage( + renderedImage.getWidth(), + renderedImage.getHeight(), + BufferedImage.TYPE_INT_RGB); } else if (format.equalsIgnoreCase("gif")) { - bufferedImage = new BufferedImage(renderedImage.getWidth(), renderedImage.getHeight(), BufferedImage.TYPE_BYTE_INDEXED); + bufferedImage = + new BufferedImage( + renderedImage.getWidth(), + renderedImage.getHeight(), + BufferedImage.TYPE_BYTE_INDEXED); } // Write image to zip file - String imageName = filename + "_" + imageIndex + " (Page " + pageNum + ")." + format; + String imageName = + filename + "_" + imageIndex + " (Page " + pageNum + ")." + format; ZipEntry zipEntry = new ZipEntry(imageName); zos.putNextEntry(zipEntry); @@ -111,7 +131,7 @@ public class ExtractImagesController { // Create ByteArrayResource from byte array byte[] zipContents = baos.toByteArray(); - return WebResponseUtils.boasToWebResponse(baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.boasToWebResponse( + baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM); } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/FakeScanControllerWIP.java b/src/main/java/stirling/software/SPDF/controller/api/misc/FakeScanControllerWIP.java index 9e9d8ba6..e9885f1e 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/FakeScanControllerWIP.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/FakeScanControllerWIP.java @@ -3,21 +3,17 @@ package stirling.software.SPDF.controller.api.misc; import java.awt.Color; import java.awt.geom.AffineTransform; import java.awt.image.AffineTransformOp; - import java.awt.image.BufferedImage; import java.awt.image.BufferedImageOp; import java.awt.image.ConvolveOp; import java.awt.image.Kernel; import java.awt.image.RescaleOp; import java.io.ByteArrayOutputStream; - import java.io.File; import java.io.IOException; import java.security.SecureRandom; - import java.util.Random; - import javax.imageio.ImageIO; import org.apache.pdfbox.pdmodel.PDDocument; @@ -40,6 +36,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.utils.WebResponseUtils; @@ -50,102 +47,101 @@ public class FakeScanControllerWIP { private static final Logger logger = LoggerFactory.getLogger(FakeScanControllerWIP.class); - //TODO + // TODO @Hidden @PostMapping(consumes = "multipart/form-data", value = "/fakeScan") @Operation( - summary = "Repair a PDF file", - description = "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response." - ) + summary = "Repair a PDF file", + description = + "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response.") public ResponseEntity repairPdf(@ModelAttribute PDFFile request) throws IOException { MultipartFile inputFile = request.getFileInput(); - PDDocument document = PDDocument.load(inputFile.getBytes()); - PDFRenderer pdfRenderer = new PDFRenderer(document); - for (int page = 0; page < document.getNumberOfPages(); ++page) - { - BufferedImage image = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB); - ImageIO.write(image, "png", new File("scanned-" + (page+1) + ".png")); - } - document.close(); + PDDocument document = PDDocument.load(inputFile.getBytes()); + PDFRenderer pdfRenderer = new PDFRenderer(document); + for (int page = 0; page < document.getNumberOfPages(); ++page) { + BufferedImage image = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB); + ImageIO.write(image, "png", new File("scanned-" + (page + 1) + ".png")); + } + document.close(); - // Constants - int scannedness = 90; // Value between 0 and 100 - int dirtiness = 0; // Value between 0 and 100 + // Constants + int scannedness = 90; // Value between 0 and 100 + int dirtiness = 0; // Value between 0 and 100 - // Load the source image - BufferedImage sourceImage = ImageIO.read(new File("scanned-1.png")); + // Load the source image + BufferedImage sourceImage = ImageIO.read(new File("scanned-1.png")); - // Create the destination image - BufferedImage destinationImage = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), sourceImage.getType()); + // Create the destination image + BufferedImage destinationImage = + new BufferedImage( + sourceImage.getWidth(), sourceImage.getHeight(), sourceImage.getType()); - // Apply a brightness and contrast effect based on the "scanned-ness" - float scaleFactor = 1.0f + (scannedness / 100.0f) * 0.5f; // Between 1.0 and 1.5 - float offset = scannedness * 1.5f; // Between 0 and 150 - BufferedImageOp op = new RescaleOp(scaleFactor, offset, null); - op.filter(sourceImage, destinationImage); + // Apply a brightness and contrast effect based on the "scanned-ness" + float scaleFactor = 1.0f + (scannedness / 100.0f) * 0.5f; // Between 1.0 and 1.5 + float offset = scannedness * 1.5f; // Between 0 and 150 + BufferedImageOp op = new RescaleOp(scaleFactor, offset, null); + op.filter(sourceImage, destinationImage); - // Apply a rotation effect - double rotationRequired = Math.toRadians((new SecureRandom().nextInt(3 - 1) + 1)); // Random angle between 1 and 3 degrees - double locationX = destinationImage.getWidth() / 2; - double locationY = destinationImage.getHeight() / 2; - AffineTransform tx = AffineTransform.getRotateInstance(rotationRequired, locationX, locationY); - AffineTransformOp rotateOp = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR); - destinationImage = rotateOp.filter(destinationImage, null); + // Apply a rotation effect + double rotationRequired = + Math.toRadians( + (new SecureRandom().nextInt(3 - 1) + + 1)); // Random angle between 1 and 3 degrees + double locationX = destinationImage.getWidth() / 2; + double locationY = destinationImage.getHeight() / 2; + AffineTransform tx = + AffineTransform.getRotateInstance(rotationRequired, locationX, locationY); + AffineTransformOp rotateOp = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR); + destinationImage = rotateOp.filter(destinationImage, null); - // Apply a blur effect based on the "scanned-ness" - float blurIntensity = scannedness / 100.0f * 0.2f; // Between 0.0 and 0.2 - float[] matrix = { - blurIntensity, blurIntensity, blurIntensity, - blurIntensity, blurIntensity, blurIntensity, - blurIntensity, blurIntensity, blurIntensity - }; - BufferedImageOp blurOp = new ConvolveOp(new Kernel(3, 3, matrix), ConvolveOp.EDGE_NO_OP, null); - destinationImage = blurOp.filter(destinationImage, null); + // Apply a blur effect based on the "scanned-ness" + float blurIntensity = scannedness / 100.0f * 0.2f; // Between 0.0 and 0.2 + float[] matrix = { + blurIntensity, blurIntensity, blurIntensity, + blurIntensity, blurIntensity, blurIntensity, + blurIntensity, blurIntensity, blurIntensity + }; + BufferedImageOp blurOp = + new ConvolveOp(new Kernel(3, 3, matrix), ConvolveOp.EDGE_NO_OP, null); + destinationImage = blurOp.filter(destinationImage, null); - // Add noise to the image based on the "dirtiness" - Random random = new SecureRandom(); - for (int y = 0; y < destinationImage.getHeight(); y++) { - for (int x = 0; x < destinationImage.getWidth(); x++) { - if (random.nextInt(100) < dirtiness) { - // Change the pixel color to black randomly based on the "dirtiness" - destinationImage.setRGB(x, y, Color.BLACK.getRGB()); - } - } - } + // Add noise to the image based on the "dirtiness" + Random random = new SecureRandom(); + for (int y = 0; y < destinationImage.getHeight(); y++) { + for (int x = 0; x < destinationImage.getWidth(); x++) { + if (random.nextInt(100) < dirtiness) { + // Change the pixel color to black randomly based on the "dirtiness" + destinationImage.setRGB(x, y, Color.BLACK.getRGB()); + } + } + } - // Save the image - ImageIO.write(destinationImage, "PNG", new File("scanned-1.png")); + // Save the image + ImageIO.write(destinationImage, "PNG", new File("scanned-1.png")); + PDDocument documentOut = new PDDocument(); + for (int page = 1; page <= document.getNumberOfPages(); ++page) { + BufferedImage bim = ImageIO.read(new File("scanned-" + page + ".png")); - - - - + // Adjust the dimensions of the page + PDPage pdPage = new PDPage(new PDRectangle(bim.getWidth() - 1, bim.getHeight() - 1)); + documentOut.addPage(pdPage); - PDDocument documentOut = new PDDocument(); - for (int page = 1; page <= document.getNumberOfPages(); ++page) - { - BufferedImage bim = ImageIO.read(new File("scanned-" + page + ".png")); - - // Adjust the dimensions of the page - PDPage pdPage = new PDPage(new PDRectangle(bim.getWidth() - 1, bim.getHeight() - 1)); - documentOut.addPage(pdPage); - - PDImageXObject pdImage = LosslessFactory.createFromImage(documentOut, bim); - PDPageContentStream contentStream = new PDPageContentStream(documentOut, pdPage); - - // Draw the image with a slight offset and enlarged dimensions - contentStream.drawImage(pdImage, -1, -1, bim.getWidth() + 2, bim.getHeight() + 2); - contentStream.close(); - } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - documentOut.save(baos); - documentOut.close(); + PDImageXObject pdImage = LosslessFactory.createFromImage(documentOut, bim); + PDPageContentStream contentStream = new PDPageContentStream(documentOut, pdPage); + + // Draw the image with a slight offset and enlarged dimensions + contentStream.drawImage(pdImage, -1, -1, bim.getWidth() + 2, bim.getHeight() + 2); + contentStream.close(); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + documentOut.save(baos); + documentOut.close(); // Return the optimized PDF as a response - String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scanned.pdf"; + String outputFilename = + inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scanned.pdf"; return WebResponseUtils.boasToWebResponse(baos, outputFilename); } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java index 027c6240..62783dc4 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java @@ -19,6 +19,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.misc.MetadataRequest; import stirling.software.SPDF.utils.WebResponseUtils; @@ -27,7 +28,6 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "Misc", description = "Miscellaneous APIs") public class MetadataController { - private String checkUndefined(String entry) { // Check if the string is "undefined" if ("undefined".equals(entry)) { @@ -36,14 +36,16 @@ public class MetadataController { } // Return the original string if it's not "undefined" return entry; - } @PostMapping(consumes = "multipart/form-data", value = "/update-metadata") - @Operation(summary = "Update metadata of a PDF file", - description = "This endpoint allows you to update the metadata of a given PDF file. You can add, modify, or delete standard and custom metadata fields. Input:PDF Output:PDF Type:SISO") - public ResponseEntity metadata(@ModelAttribute MetadataRequest request) throws IOException { - + @Operation( + summary = "Update metadata of a PDF file", + description = + "This endpoint allows you to update the metadata of a given PDF file. You can add, modify, or delete standard and custom metadata fields. Input:PDF Output:PDF Type:SISO") + public ResponseEntity metadata(@ModelAttribute MetadataRequest request) + throws IOException { + // Extract PDF file from the request object MultipartFile pdfFile = request.getFileInput(); @@ -61,8 +63,8 @@ public class MetadataController { // Extract additional custom parameters Map allRequestParams = request.getAllRequestParams(); - if(allRequestParams == null) { - allRequestParams = new java.util.HashMap(); + if (allRequestParams == null) { + allRequestParams = new java.util.HashMap(); } // Load the PDF file into a PDDocument PDDocument document = PDDocument.load(pdfFile.getBytes()); @@ -89,7 +91,9 @@ public class MetadataController { } // Remove metadata from the PDF history document.getDocumentCatalog().getCOSObject().removeItem(COSName.getPDFName("Metadata")); - document.getDocumentCatalog().getCOSObject().removeItem(COSName.getPDFName("PieceInfo")); + document.getDocumentCatalog() + .getCOSObject() + .removeItem(COSName.getPDFName("PieceInfo")); author = null; creationDate = null; creator = null; @@ -104,9 +108,17 @@ public class MetadataController { for (Entry entry : allRequestParams.entrySet()) { String key = entry.getKey(); // Check if the key is a standard metadata key - if (!key.equalsIgnoreCase("Author") && !key.equalsIgnoreCase("CreationDate") && !key.equalsIgnoreCase("Creator") && !key.equalsIgnoreCase("Keywords") - && !key.equalsIgnoreCase("modificationDate") && !key.equalsIgnoreCase("Producer") && !key.equalsIgnoreCase("Subject") && !key.equalsIgnoreCase("Title") - && !key.equalsIgnoreCase("Trapped") && !key.contains("customKey") && !key.contains("customValue")) { + if (!key.equalsIgnoreCase("Author") + && !key.equalsIgnoreCase("CreationDate") + && !key.equalsIgnoreCase("Creator") + && !key.equalsIgnoreCase("Keywords") + && !key.equalsIgnoreCase("modificationDate") + && !key.equalsIgnoreCase("Producer") + && !key.equalsIgnoreCase("Subject") + && !key.equalsIgnoreCase("Title") + && !key.equalsIgnoreCase("Trapped") + && !key.contains("customKey") + && !key.contains("customValue")) { info.setCustomMetadataValue(key, entry.getValue()); } else if (key.contains("customKey")) { int number = Integer.parseInt(key.replaceAll("\\D", "")); @@ -119,7 +131,8 @@ public class MetadataController { if (creationDate != null && creationDate.length() > 0) { Calendar creationDateCal = Calendar.getInstance(); try { - creationDateCal.setTime(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(creationDate)); + creationDateCal.setTime( + new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(creationDate)); } catch (ParseException e) { e.printStackTrace(); } @@ -130,7 +143,8 @@ public class MetadataController { if (modificationDate != null && modificationDate.length() > 0) { Calendar modificationDateCal = Calendar.getInstance(); try { - modificationDateCal.setTime(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(modificationDate)); + modificationDateCal.setTime( + new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(modificationDate)); } catch (ParseException e) { e.printStackTrace(); } @@ -147,7 +161,8 @@ public class MetadataController { info.setTrapped(trapped); document.setDocumentInformation(info); - return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_metadata.pdf"); + return WebResponseUtils.pdfDocToWebResponse( + document, + pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_metadata.pdf"); } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java index 5ea1818e..21cf2b1c 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java @@ -26,6 +26,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest; import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; @@ -44,14 +45,21 @@ public class OCRController { if (files == null) { return Collections.emptyList(); } - return Arrays.stream(files).filter(file -> file.getName().endsWith(".traineddata")).map(file -> file.getName().replace(".traineddata", "")) - .filter(lang -> !lang.equalsIgnoreCase("osd")).collect(Collectors.toList()); + return Arrays.stream(files) + .filter(file -> file.getName().endsWith(".traineddata")) + .map(file -> file.getName().replace(".traineddata", "")) + .filter(lang -> !lang.equalsIgnoreCase("osd")) + .collect(Collectors.toList()); } @PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf") - @Operation(summary = "Process a PDF file with OCR", - description = "This endpoint processes a PDF file using OCR (Optical Character Recognition). Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType, and removeImagesAfter options. Input:PDF Output:PDF Type:SI-Conditional") - public ResponseEntity processPdfWithOCR(@ModelAttribute ProcessPdfWithOcrRequest request) throws IOException, InterruptedException { + @Operation( + summary = "Process a PDF file with OCR", + description = + "This endpoint processes a PDF file using OCR (Optical Character Recognition). Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType, and removeImagesAfter options. Input:PDF Output:PDF Type:SI-Conditional") + public ResponseEntity processPdfWithOCR( + @ModelAttribute ProcessPdfWithOcrRequest request) + throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); List selectedLanguages = request.getLanguages(); Boolean sidecar = request.isSidecar(); @@ -65,16 +73,17 @@ public class OCRController { if (selectedLanguages == null || selectedLanguages.isEmpty()) { throw new IOException("Please select at least one language."); } - - if(!ocrRenderType.equals("hocr") && !ocrRenderType.equals("sandwich")) { + + if (!ocrRenderType.equals("hocr") && !ocrRenderType.equals("sandwich")) { throw new IOException("ocrRenderType wrong"); } - + // Get available Tesseract languages List availableLanguages = getAvailableTesseractLanguages(); // Validate selected languages - selectedLanguages = selectedLanguages.stream().filter(availableLanguages::contains).toList(); + selectedLanguages = + selectedLanguages.stream().filter(availableLanguages::contains).toList(); if (selectedLanguages.isEmpty()) { throw new IOException("None of the selected languages are valid."); @@ -92,8 +101,16 @@ public class OCRController { // Run OCR Command String languageOption = String.join("+", selectedLanguages); - - List command = new ArrayList<>(Arrays.asList("ocrmypdf", "--verbose", "2", "--output-type", "pdf", "--pdf-renderer" , ocrRenderType)); + List command = + new ArrayList<>( + Arrays.asList( + "ocrmypdf", + "--verbose", + "2", + "--output-type", + "pdf", + "--pdf-renderer", + ocrRenderType)); if (sidecar != null && sidecar) { sidecarTextPath = Files.createTempFile("sidecar", ".txt"); @@ -120,42 +137,61 @@ public class OCRController { } } - command.addAll(Arrays.asList("--language", languageOption, tempInputFile.toString(), tempOutputFile.toString())); + command.addAll( + Arrays.asList( + "--language", + languageOption, + tempInputFile.toString(), + tempOutputFile.toString())); // Run CLI command - ProcessExecutorResult result = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command); - if(result.getRc() != 0 && result.getMessages().contains("multiprocessing/synchronize.py") && result.getMessages().contains("OSError: [Errno 38] Function not implemented")) { - command.add("--jobs"); - command.add("1"); - result = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command); + ProcessExecutorResult result = + ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF) + .runCommandWithOutputHandling(command); + if (result.getRc() != 0 + && result.getMessages().contains("multiprocessing/synchronize.py") + && result.getMessages().contains("OSError: [Errno 38] Function not implemented")) { + command.add("--jobs"); + command.add("1"); + result = + ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF) + .runCommandWithOutputHandling(command); } - - - // Remove images from the OCR processed PDF if the flag is set to true if (removeImagesAfter != null && removeImagesAfter) { Path tempPdfWithoutImages = Files.createTempFile("output_", "_no_images.pdf"); - List gsCommand = Arrays.asList("gs", "-sDEVICE=pdfwrite", "-dFILTERIMAGE", "-o", tempPdfWithoutImages.toString(), tempOutputFile.toString()); + List gsCommand = + Arrays.asList( + "gs", + "-sDEVICE=pdfwrite", + "-dFILTERIMAGE", + "-o", + tempPdfWithoutImages.toString(), + tempOutputFile.toString()); - ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(gsCommand); + ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT) + .runCommandWithOutputHandling(gsCommand); tempOutputFile = tempPdfWithoutImages; } // Read the OCR processed PDF file byte[] pdfBytes = Files.readAllBytes(tempOutputFile); // Clean up the temporary files Files.delete(tempInputFile); - + // Return the OCR processed PDF as a response - String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.pdf"; + String outputFilename = + inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.pdf"; if (sidecar != null && sidecar) { // Create a zip file containing both the PDF and the text file - String outputZipFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.zip"; + String outputZipFilename = + inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.zip"; Path tempZipFile = Files.createTempFile("output_", ".zip"); - try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) { + try (ZipOutputStream zipOut = + new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) { // Add PDF file to the zip ZipEntry pdfEntry = new ZipEntry(outputFilename); zipOut.putNextEntry(pdfEntry); @@ -177,13 +213,12 @@ public class OCRController { Files.delete(sidecarTextPath); // Return the zip file containing both the PDF and the text file - return WebResponseUtils.bytesToWebResponse(zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.bytesToWebResponse( + zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); } else { // Return the OCR processed PDF as a response Files.delete(tempOutputFile); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); } - } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java index e28f7535..9fe6249c 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java @@ -14,6 +14,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.misc.OverlayImageRequest; import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.WebResponseUtils; @@ -27,9 +28,9 @@ public class OverlayImageController { @PostMapping(consumes = "multipart/form-data", value = "/add-image") @Operation( - summary = "Overlay image onto a PDF file", - description = "This endpoint overlays an image onto a PDF file at the specified coordinates. The image can be overlaid on every page of the PDF if specified. Input:PDF/IMAGE Output:PDF Type:MF-SISO" - ) + summary = "Overlay image onto a PDF file", + description = + "This endpoint overlays an image onto a PDF file at the specified coordinates. The image can be overlaid on every page of the PDF if specified. Input:PDF/IMAGE Output:PDF Type:MF-SISO") public ResponseEntity overlayImage(@ModelAttribute OverlayImageRequest request) { MultipartFile pdfFile = request.getFileInput(); MultipartFile imageFile = request.getImageFile(); @@ -41,7 +42,9 @@ public class OverlayImageController { byte[] imageBytes = imageFile.getBytes(); byte[] result = PdfUtils.overlayImage(pdfBytes, imageBytes, x, y, everyPage); - return WebResponseUtils.bytesToWebResponse(result, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf"); + return WebResponseUtils.bytesToWebResponse( + result, + pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf"); } catch (IOException e) { logger.error("Failed to add image to PDF", e); return new ResponseEntity<>(HttpStatus.BAD_REQUEST); diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java index 61a1ec97..6c302524 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java @@ -21,6 +21,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.misc.AddPageNumbersRequest; import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.WebResponseUtils; @@ -33,16 +34,20 @@ 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(@ModelAttribute AddPageNumbersRequest request) throws IOException { + @Operation( + summary = "Add page numbers to a PDF document", + description = + "This operation takes an input PDF file and adds page numbers to it. Input:PDF Output:PDF Type:SISO") + public ResponseEntity addPageNumbers(@ModelAttribute AddPageNumbersRequest request) + throws IOException { MultipartFile file = request.getFileInput(); String customMargin = request.getCustomMargin(); int position = request.getPosition(); int startingNumber = request.getStartingNumber(); String pagesToNumber = request.getPagesToNumber(); String customText = request.getCustomText(); - int pageNumber = startingNumber; - byte[] fileBytes = file.getBytes(); + int pageNumber = startingNumber; + byte[] fileBytes = file.getBytes(); PDDocument document = PDDocument.load(fileBytes); float marginFactor; @@ -58,9 +63,8 @@ public class PageNumbersController { break; case "x-large": marginFactor = 0.075f; - break; - - + break; + default: marginFactor = 0.035f; break; @@ -68,19 +72,29 @@ public class PageNumbersController { float fontSize = 12.0f; PDType1Font font = PDType1Font.HELVETICA; - if(pagesToNumber == null || pagesToNumber.length() == 0) { - pagesToNumber = "all"; + if (pagesToNumber == null || pagesToNumber.length() == 0) { + pagesToNumber = "all"; } - if(customText == null || customText.length() == 0) { - customText = "{n}"; + if (customText == null || customText.length() == 0) { + customText = "{n}"; } - List pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages()); + List pagesToNumberList = + GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages()); for (int i : pagesToNumberList) { PDPage page = document.getPage(i); PDRectangle pageSize = page.getMediaBox(); - String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(document.getNumberOfPages())).replace("{filename}", file.getOriginalFilename().replaceFirst("[.][^.]+$", "")) : String.valueOf(pageNumber); + String text = + customText != null + ? customText + .replace("{n}", String.valueOf(pageNumber)) + .replace("{total}", String.valueOf(document.getNumberOfPages())) + .replace( + "{filename}", + file.getOriginalFilename() + .replaceFirst("[.][^.]+$", "")) + : String.valueOf(pageNumber); float x, y; @@ -88,10 +102,10 @@ public class PageNumbersController { int yGroup = 2 - (position - 1) / 3; switch (xGroup) { - case 0: // left + case 0: // left x = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth(); break; - case 1: // center + case 1: // center x = pageSize.getLowerLeftX() + (pageSize.getWidth() / 2); break; default: // right @@ -100,10 +114,10 @@ public class PageNumbersController { } switch (yGroup) { - case 0: // bottom + case 0: // bottom y = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight(); break; - case 1: // middle + case 1: // middle y = pageSize.getLowerLeftY() + (pageSize.getHeight() / 2); break; default: // top @@ -111,7 +125,9 @@ public class PageNumbersController { break; } - PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true); + PDPageContentStream contentStream = + new PDPageContentStream( + document, page, PDPageContentStream.AppendMode.APPEND, true); contentStream.beginText(); contentStream.setFont(font, fontSize); contentStream.newLineAtOffset(x, y); @@ -126,10 +142,9 @@ public class PageNumbersController { document.save(baos); document.close(); - return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", MediaType.APPLICATION_PDF); - + return WebResponseUtils.bytesToWebResponse( + baos.toByteArray(), + file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", + MediaType.APPLICATION_PDF); } - - - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java index f9ae541d..112985a3 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java @@ -17,6 +17,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; @@ -31,11 +32,12 @@ public class RepairController { @PostMapping(consumes = "multipart/form-data", value = "/repair") @Operation( - summary = "Repair a PDF file", - description = "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response. Input:PDF Output:PDF Type:SISO" - ) - public ResponseEntity repairPdf(@ModelAttribute PDFFile request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); + summary = "Repair a PDF file", + description = + "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response. Input:PDF Output:PDF Type:SISO") + public ResponseEntity repairPdf(@ModelAttribute PDFFile request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); // Save the uploaded file to a temporary location Path tempInputFile = Files.createTempFile("input_", ".pdf"); inputFile.transferTo(tempInputFile.toFile()); @@ -50,8 +52,9 @@ public class RepairController { command.add("-sDEVICE=pdfwrite"); command.add(tempInputFile.toString()); - - ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(command); + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT) + .runCommandWithOutputHandling(command); // Read the optimized PDF file byte[] pdfBytes = Files.readAllBytes(tempOutputFile); @@ -61,8 +64,8 @@ public class RepairController { Files.delete(tempOutputFile); // Return the optimized PDF as a response - String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_repaired.pdf"; + String outputFilename = + inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_repaired.pdf"; return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java b/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java index cef32ac7..ed7852fa 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java @@ -17,47 +17,60 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/misc") @Tag(name = "Misc", description = "Miscellaneous APIs") public class ShowJavascript { private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class); + @PostMapping(consumes = "multipart/form-data", value = "/show-javascript") - @Operation(summary = "Grabs all JS from a PDF and returns a single JS file with all code", description = "desc. Input:PDF Output:JS Type:SISO") + @Operation( + summary = "Grabs all JS from a PDF and returns a single JS file with all code", + description = "desc. Input:PDF Output:JS Type:SISO") public ResponseEntity extractHeader(@ModelAttribute PDFFile request) throws Exception { - MultipartFile inputFile = request.getFileInput(); + MultipartFile inputFile = request.getFileInput(); String script = ""; try (PDDocument document = PDDocument.load(inputFile.getInputStream())) { - - if(document.getDocumentCatalog() != null && document.getDocumentCatalog().getNames() != null) { - PDNameTreeNode jsTree = document.getDocumentCatalog().getNames().getJavaScript(); - - if (jsTree != null) { - Map jsEntries = jsTree.getNames(); - - for (Map.Entry entry : jsEntries.entrySet()) { - String name = entry.getKey(); - PDActionJavaScript jsAction = entry.getValue(); - String jsCodeStr = jsAction.getAction(); - - script += "// File: " + inputFile.getOriginalFilename() + ", Script: " + name + "\n" + jsCodeStr + "\n"; - } - } - } - if (script.isEmpty()) { - script = "PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript"; + if (document.getDocumentCatalog() != null + && document.getDocumentCatalog().getNames() != null) { + PDNameTreeNode jsTree = + document.getDocumentCatalog().getNames().getJavaScript(); + + if (jsTree != null) { + Map jsEntries = jsTree.getNames(); + + for (Map.Entry entry : jsEntries.entrySet()) { + String name = entry.getKey(); + PDActionJavaScript jsAction = entry.getValue(); + String jsCodeStr = jsAction.getAction(); + + script += + "// File: " + + inputFile.getOriginalFilename() + + ", Script: " + + name + + "\n" + + jsCodeStr + + "\n"; + } + } } - return WebResponseUtils.bytesToWebResponse(script.getBytes(StandardCharsets.UTF_8), inputFile.getOriginalFilename() + ".js"); + if (script.isEmpty()) { + script = + "PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript"; + } + + return WebResponseUtils.bytesToWebResponse( + script.getBytes(StandardCharsets.UTF_8), + inputFile.getOriginalFilename() + ".js"); } } - - - - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/ApiDocService.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/ApiDocService.java index c3bf9c8d..6ed5f51d 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/ApiDocService.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/ApiDocService.java @@ -1,9 +1,11 @@ package stirling.software.SPDF.controller.api.pipeline; + import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; @@ -17,44 +19,39 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.ServletContext; - import stirling.software.SPDF.SPdfApplication; import stirling.software.SPDF.model.ApiEndpoint; import stirling.software.SPDF.model.Role; -import org.slf4j.Logger; + @Service public class ApiDocService { private final Map apiDocumentation = new HashMap<>(); private static final Logger logger = LoggerFactory.getLogger(ApiDocService.class); - - @Autowired - private ServletContext servletContext; + + @Autowired private ServletContext servletContext; private String getApiDocsUrl() { String contextPath = servletContext.getContextPath(); String port = SPdfApplication.getPort(); - return "http://localhost:"+ port + contextPath + "/v1/api-docs"; + return "http://localhost:" + port + contextPath + "/v1/api-docs"; } - - @Autowired(required=false) - private UserServiceInterface userService; + @Autowired(required = false) + private UserServiceInterface userService; - private String getApiKeyForUser() { - if(userService == null) - return ""; - return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId()); - } - - JsonNode apiDocsJsonRootNode; - - - //@EventListener(ApplicationReadyEvent.class) - private synchronized void loadApiDocumentation() { - String apiDocsJson = ""; + private String getApiKeyForUser() { + if (userService == null) return ""; + return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId()); + } + + JsonNode apiDocsJsonRootNode; + + // @EventListener(ApplicationReadyEvent.class) + private synchronized void loadApiDocumentation() { + String apiDocsJson = ""; try { HttpHeaders headers = new HttpHeaders(); String apiKey = getApiKeyForUser(); @@ -64,49 +61,52 @@ public class ApiDocService { HttpEntity entity = new HttpEntity<>(headers); RestTemplate restTemplate = new RestTemplate(); - ResponseEntity response = restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class); + ResponseEntity response = + restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class); apiDocsJson = response.getBody(); ObjectMapper mapper = new ObjectMapper(); apiDocsJsonRootNode = mapper.readTree(apiDocsJson); JsonNode paths = apiDocsJsonRootNode.path("paths"); - paths.fields().forEachRemaining(entry -> { - String path = entry.getKey(); - JsonNode pathNode = entry.getValue(); - if (pathNode.has("post")) { - JsonNode postNode = pathNode.get("post"); - ApiEndpoint endpoint = new ApiEndpoint(path, postNode); - apiDocumentation.put(path, endpoint); - } - }); + paths.fields() + .forEachRemaining( + entry -> { + String path = entry.getKey(); + JsonNode pathNode = entry.getValue(); + if (pathNode.has("post")) { + JsonNode postNode = pathNode.get("post"); + ApiEndpoint endpoint = new ApiEndpoint(path, postNode); + apiDocumentation.put(path, endpoint); + } + }); } catch (Exception e) { // Handle exceptions - logger.error("Error grabbing swagger doc, body result {}", apiDocsJson); + logger.error("Error grabbing swagger doc, body result {}", apiDocsJson); } } public boolean isValidOperation(String operationName, Map parameters) { - if(apiDocumentation.size() == 0) { - loadApiDocumentation(); - } + if (apiDocumentation.size() == 0) { + loadApiDocumentation(); + } if (!apiDocumentation.containsKey(operationName)) { return false; } ApiEndpoint endpoint = apiDocumentation.get(operationName); return endpoint.areParametersValid(parameters); } - + public boolean isMultiInput(String operationName) { - if(apiDocsJsonRootNode == null || apiDocumentation.size() == 0) { - loadApiDocumentation(); - } - if (!apiDocumentation.containsKey(operationName)) { + if (apiDocsJsonRootNode == null || apiDocumentation.size() == 0) { + loadApiDocumentation(); + } + if (!apiDocumentation.containsKey(operationName)) { return false; } ApiEndpoint endpoint = apiDocumentation.get(operationName); - String description = endpoint.getDescription(); + String description = endpoint.getDescription(); Pattern pattern = Pattern.compile("Type:(\\w+)"); Matcher matcher = pattern.matcher(description); @@ -115,9 +115,8 @@ public class ApiDocService { return type.startsWith("MI"); } - return false; + return false; } } // Model class for API Endpoint - 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 index a6f78a6f..db5e9661 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.api.HandleDataRequest; @@ -34,84 +35,80 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "Pipeline", description = "Pipeline APIs") public class PipelineController { - private static final Logger logger = LoggerFactory.getLogger(PipelineController.class); + private static final Logger logger = LoggerFactory.getLogger(PipelineController.class); - final String watchedFoldersDir = "./pipeline/watchedFolders/"; - final String finishedFoldersDir = "./pipeline/finishedFolders/"; - @Autowired - PipelineProcessor processor; - + final String watchedFoldersDir = "./pipeline/watchedFolders/"; + final String finishedFoldersDir = "./pipeline/finishedFolders/"; + @Autowired PipelineProcessor processor; - @Autowired - ApplicationProperties applicationProperties; - - @Autowired - private ObjectMapper objectMapper; - + @Autowired ApplicationProperties applicationProperties; - @PostMapping("/handleData") - public ResponseEntity handleData(@ModelAttribute HandleDataRequest request) throws JsonMappingException, JsonProcessingException { - if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) { - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); - } + @Autowired private ObjectMapper objectMapper; - MultipartFile[] files = request.getFileInput(); - String jsonString = request.getJson(); - if (files == null) { - return null; - } - PipelineConfig config = objectMapper.readValue(jsonString, PipelineConfig.class); - logger.info("Received POST request to /handleData with {} files", files.length); - try { - List inputFiles = processor.generateInputFiles(files); - if(inputFiles == null || inputFiles.size() == 0) { - return null; + @PostMapping("/handleData") + public ResponseEntity handleData(@ModelAttribute HandleDataRequest request) + throws JsonMappingException, JsonProcessingException { + if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + MultipartFile[] files = request.getFileInput(); + String jsonString = request.getJson(); + if (files == null) { + return null; + } + PipelineConfig config = objectMapper.readValue(jsonString, PipelineConfig.class); + logger.info("Received POST request to /handleData with {} files", files.length); + try { + List inputFiles = processor.generateInputFiles(files); + if (inputFiles == null || inputFiles.size() == 0) { + return null; } - List outputFiles = processor.runPipelineAgainstFiles(inputFiles, config); - if (outputFiles != null && outputFiles.size() == 1) { - // If there is only one file, return it directly - Resource singleFile = outputFiles.get(0); - InputStream is = singleFile.getInputStream(); - byte[] bytes = new byte[(int) singleFile.contentLength()]; - is.read(bytes); - is.close(); + List outputFiles = processor.runPipelineAgainstFiles(inputFiles, config); + if (outputFiles != null && outputFiles.size() == 1) { + // If there is only one file, return it directly + Resource singleFile = outputFiles.get(0); + InputStream is = singleFile.getInputStream(); + byte[] bytes = new byte[(int) singleFile.contentLength()]; + is.read(bytes); + is.close(); - logger.info("Returning single file response..."); - return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(), - MediaType.APPLICATION_OCTET_STREAM); - } else if (outputFiles == null) { - return null; - } + 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); + // 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); + // 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); + // 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(); + // Write the bytes of the file to the zip + zipOut.write(bytes, 0, bytes.length); + zipOut.closeEntry(); - is.close(); - } + 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; - } - } + 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; + } + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java index dc45d4cb..80fdd71c 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java @@ -33,50 +33,48 @@ import stirling.software.SPDF.model.PipelineOperation; @Service public class PipelineDirectoryProcessor { - private static final Logger logger = LoggerFactory.getLogger(PipelineDirectoryProcessor.class); - @Autowired - private ObjectMapper objectMapper; - @Autowired - private ApiDocService apiDocService; - @Autowired - private ApplicationProperties applicationProperties; - + private static final Logger logger = LoggerFactory.getLogger(PipelineDirectoryProcessor.class); + @Autowired private ObjectMapper objectMapper; + @Autowired private ApiDocService apiDocService; + @Autowired private ApplicationProperties applicationProperties; + final String watchedFoldersDir = "./pipeline/watchedFolders/"; - final String finishedFoldersDir = "./pipeline/finishedFolders/"; - - @Autowired - PipelineProcessor processor; + final String finishedFoldersDir = "./pipeline/finishedFolders/"; + + @Autowired PipelineProcessor processor; @Scheduled(fixedRate = 60000) - public void scanFolders() { - if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) { - return; - } - Path watchedFolderPath = Paths.get(watchedFoldersDir); - if (!Files.exists(watchedFolderPath)) { - try { - Files.createDirectories(watchedFolderPath); - logger.info("Created directory: {}", watchedFolderPath); - } catch (IOException e) { - logger.error("Error creating directory: {}", watchedFolderPath, e); - return; - } - } - try (Stream paths = Files.walk(watchedFolderPath)) { - paths.filter(Files::isDirectory).forEach(t -> { - try { - if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) { - handleDirectory(t); - } - } catch (Exception e) { - logger.error("Error handling directory: {}", t, e); - } - }); - } catch (Exception e) { - logger.error("Error walking through directory: {}", watchedFolderPath, e); - } - } - + public void scanFolders() { + if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) { + return; + } + Path watchedFolderPath = Paths.get(watchedFoldersDir); + if (!Files.exists(watchedFolderPath)) { + try { + Files.createDirectories(watchedFolderPath); + logger.info("Created directory: {}", watchedFolderPath); + } catch (IOException e) { + logger.error("Error creating directory: {}", watchedFolderPath, e); + return; + } + } + try (Stream paths = Files.walk(watchedFolderPath)) { + paths.filter(Files::isDirectory) + .forEach( + t -> { + try { + if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) { + handleDirectory(t); + } + } catch (Exception e) { + logger.error("Error handling directory: {}", t, e); + } + }); + } catch (Exception e) { + logger.error("Error walking through directory: {}", watchedFolderPath, e); + } + } + public void handleDirectory(Path dir) throws IOException { logger.info("Handling directory: {}", dir); Path processingDir = createProcessingDirectory(dir); @@ -113,13 +111,14 @@ public class PipelineDirectoryProcessor { return objectMapper.readValue(jsonString, PipelineConfig.class); } - private void processPipelineOperations(Path dir, Path processingDir, Path jsonFile, PipelineConfig config) throws IOException { + private void processPipelineOperations( + Path dir, Path processingDir, Path jsonFile, PipelineConfig config) throws IOException { for (PipelineOperation operation : config.getOperations()) { validateOperation(operation); File[] files = collectFilesForProcessing(dir, jsonFile, operation); - if(files == null || files.length == 0) { - logger.debug("No files detected for {} ", dir); - return; + if (files == null || files.length == 0) { + logger.debug("No files detected for {} ", dir); + return; } List filesToProcess = prepareFilesForProcessing(files, processingDir); runPipelineAgainstFiles(filesToProcess, config, dir, processingDir); @@ -132,20 +131,22 @@ public class PipelineDirectoryProcessor { } } - private File[] collectFilesForProcessing(Path dir, Path jsonFile, PipelineOperation operation) throws IOException { + private File[] collectFilesForProcessing(Path dir, Path jsonFile, PipelineOperation operation) + throws IOException { try (Stream paths = Files.list(dir)) { if ("automated".equals(operation.getParameters().get("fileInput"))) { return paths.filter(path -> !Files.isDirectory(path) && !path.equals(jsonFile)) - .map(Path::toFile) - .toArray(File[]::new); + .map(Path::toFile) + .toArray(File[]::new); } else { String fileInput = (String) operation.getParameters().get("fileInput"); - return new File[]{new File(fileInput)}; + return new File[] {new File(fileInput)}; } } } - private List prepareFilesForProcessing(File[] files, Path processingDir) throws IOException { + private List prepareFilesForProcessing(File[] files, Path processingDir) + throws IOException { List filesToProcess = new ArrayList<>(); for (File file : files) { Path targetPath = resolveUniqueFilePath(processingDir, file.getName()); @@ -173,27 +174,33 @@ public class PipelineDirectoryProcessor { if (dotIndex == -1) { return originalFileName + suffix; } else { - return originalFileName.substring(0, dotIndex) + suffix + originalFileName.substring(dotIndex); + return originalFileName.substring(0, dotIndex) + + suffix + + originalFileName.substring(dotIndex); } } - private void runPipelineAgainstFiles(List filesToProcess, PipelineConfig config, Path dir, Path processingDir) throws IOException { + private void runPipelineAgainstFiles( + List filesToProcess, PipelineConfig config, Path dir, Path processingDir) + throws IOException { try { - List inputFiles = processor.generateInputFiles(filesToProcess.toArray(new File[0])); - if(inputFiles == null || inputFiles.size() == 0) { - return; + List inputFiles = + processor.generateInputFiles(filesToProcess.toArray(new File[0])); + if (inputFiles == null || inputFiles.size() == 0) { + return; } - List outputFiles = processor.runPipelineAgainstFiles(inputFiles, config); + List outputFiles = processor.runPipelineAgainstFiles(inputFiles, config); if (outputFiles == null) return; moveAndRenameFiles(outputFiles, config, dir); deleteOriginalFiles(filesToProcess, processingDir); } catch (Exception e) { - logger.error("error during processing", e); + logger.error("error during processing", e); moveFilesBack(filesToProcess, processingDir); } } - private void moveAndRenameFiles(List resources, PipelineConfig config, Path dir) throws IOException { + private void moveAndRenameFiles(List resources, PipelineConfig config, Path dir) + throws IOException { for (Resource resource : resources) { String outputFileName = createOutputFileName(resource, config); Path outputPath = determineOutputPath(config, dir); @@ -217,26 +224,36 @@ public class PipelineDirectoryProcessor { String baseName = resourceName.substring(0, resourceName.lastIndexOf('.')); String extension = resourceName.substring(resourceName.lastIndexOf('.') + 1); - String outputFileName = config.getOutputPattern() - .replace("{filename}", baseName) - .replace("{pipelineName}", config.getName()) - .replace("{date}", LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))) - .replace("{time}", LocalTime.now().format(DateTimeFormatter.ofPattern("HHmmss"))) - + "." + extension; + String outputFileName = + config.getOutputPattern() + .replace("{filename}", baseName) + .replace("{pipelineName}", config.getName()) + .replace( + "{date}", + LocalDate.now() + .format(DateTimeFormatter.ofPattern("yyyyMMdd"))) + .replace( + "{time}", + LocalTime.now() + .format(DateTimeFormatter.ofPattern("HHmmss"))) + + "." + + extension; return outputFileName; } private Path determineOutputPath(PipelineConfig config, Path dir) { - String outputDir = config.getOutputDir() - .replace("{outputFolder}", finishedFoldersDir) - .replace("{folderName}", dir.toString()) - .replaceAll("\\\\?watchedFolders", ""); + String outputDir = + config.getOutputDir() + .replace("{outputFolder}", finishedFoldersDir) + .replace("{folderName}", dir.toString()) + .replaceAll("\\\\?watchedFolders", ""); return Paths.get(outputDir).isAbsolute() ? Paths.get(outputDir) : Paths.get(".", outputDir); } - private void deleteOriginalFiles(List filesToProcess, Path processingDir) throws IOException { + private void deleteOriginalFiles(List filesToProcess, Path processingDir) + throws IOException { for (File file : filesToProcess) { Files.deleteIfExists(processingDir.resolve(file.getName())); logger.info("Deleted original file: {}", file.getName()); @@ -247,12 +264,13 @@ public class PipelineDirectoryProcessor { for (File file : filesToProcess) { try { Files.move(processingDir.resolve(file.getName()), file.toPath()); - logger.info("Moved file back to original location: {} , {}",file.toPath(), file.getName()); + logger.info( + "Moved file back to original location: {} , {}", + file.toPath(), + file.getName()); } catch (IOException e) { logger.error("Error moving file back to original location: {}", file.getName(), e); } } } - - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java index 8b4b2ef4..534f3d3b 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java @@ -34,7 +34,6 @@ import org.springframework.web.client.RestTemplate; import org.springframework.web.multipart.MultipartFile; import jakarta.servlet.ServletContext; - import stirling.software.SPDF.SPdfApplication; import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineOperation; @@ -43,152 +42,160 @@ import stirling.software.SPDF.model.Role; @Service public class PipelineProcessor { - private static final Logger logger = LoggerFactory.getLogger(PipelineProcessor.class); + private static final Logger logger = LoggerFactory.getLogger(PipelineProcessor.class); + @Autowired private ApiDocService apiDocService; - @Autowired - private ApiDocService apiDocService; - - @Autowired(required=false) + @Autowired(required = false) private UserServiceInterface userService; - - @Autowired - private ServletContext servletContext; - + @Autowired private ServletContext servletContext; + private String getApiKeyForUser() { + if (userService == null) return ""; + return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId()); + } - private String getApiKeyForUser() { - if (userService == null) - return ""; - return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId()); - } + private String getBaseUrl() { + String contextPath = servletContext.getContextPath(); + String port = SPdfApplication.getPort(); + return "http://localhost:" + port + contextPath + "/"; + } - private String getBaseUrl() { - String contextPath = servletContext.getContextPath(); - String port = SPdfApplication.getPort(); + List runPipelineAgainstFiles(List outputFiles, PipelineConfig config) + throws Exception { - return "http://localhost:" + port + contextPath + "/"; - } + ByteArrayOutputStream logStream = new ByteArrayOutputStream(); + PrintStream logPrintStream = new PrintStream(logStream); - - - List runPipelineAgainstFiles(List outputFiles, PipelineConfig config) throws Exception { + boolean hasErrors = false; - ByteArrayOutputStream logStream = new ByteArrayOutputStream(); - PrintStream logPrintStream = new PrintStream(logStream); + for (PipelineOperation pipelineOperation : config.getOperations()) { + String operation = pipelineOperation.getOperation(); + boolean isMultiInputOperation = apiDocService.isMultiInput(operation); - boolean hasErrors = false; + logger.info( + "Running operation: {} isMultiInputOperation {}", + operation, + isMultiInputOperation); + Map parameters = pipelineOperation.getParameters(); + String inputFileExtension = ""; - for (PipelineOperation pipelineOperation : config.getOperations()) { - String operation = pipelineOperation.getOperation(); - boolean isMultiInputOperation = apiDocService.isMultiInput(operation); + // TODO + // if (operationNode.has("inputFileType")) { + // inputFileExtension = operationNode.get("inputFileType").asText(); + // } else { + inputFileExtension = ".pdf"; + // } + final String finalInputFileExtension = inputFileExtension; - logger.info("Running operation: {} isMultiInputOperation {}", operation, isMultiInputOperation); - Map parameters = pipelineOperation.getParameters(); - String inputFileExtension = ""; - - //TODO - //if (operationNode.has("inputFileType")) { - // inputFileExtension = operationNode.get("inputFileType").asText(); - //} else { - inputFileExtension = ".pdf"; - //} - final String finalInputFileExtension = inputFileExtension; - - String url = getBaseUrl() + operation; - - List newOutputFiles = new ArrayList<>(); - if (!isMultiInputOperation) { - for (Resource file : outputFiles) { - boolean hasInputFileType = false; - if (file.getFilename().endsWith(inputFileExtension)) { - hasInputFileType = true; - MultiValueMap body = new LinkedMultiValueMap<>(); - body.add("fileInput", file); + String url = getBaseUrl() + operation; - - for(Entry entry : parameters.entrySet()) { - body.add(entry.getKey(), entry.getValue()); - } + List newOutputFiles = new ArrayList<>(); + if (!isMultiInputOperation) { + for (Resource file : outputFiles) { + boolean hasInputFileType = false; + if (file.getFilename().endsWith(inputFileExtension)) { + hasInputFileType = true; + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("fileInput", file); - ResponseEntity response = sendWebRequest(url, body); + for (Entry entry : parameters.entrySet()) { + body.add(entry.getKey(), entry.getValue()); + } - // 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; - } + ResponseEntity response = sendWebRequest(url, body); - if (!response.getStatusCode().equals(HttpStatus.OK)) { - logPrintStream.println("Error: " + response.getBody()); - hasErrors = true; - continue; - } - processOutputFiles(operation, file.getFilename(), response, newOutputFiles); - - } + // 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 (!hasInputFileType) { - logPrintStream.println( - "No files with extension " + inputFileExtension + " found for operation " + operation); - hasErrors = true; - } + if (!response.getStatusCode().equals(HttpStatus.OK)) { + logPrintStream.println("Error: " + response.getBody()); + hasErrors = true; + continue; + } + processOutputFiles(operation, file.getFilename(), response, newOutputFiles); + } - outputFiles = newOutputFiles; - } + if (!hasInputFileType) { + logPrintStream.println( + "No files with extension " + + inputFileExtension + + " found for operation " + + operation); + hasErrors = true; + } - } else { - // Filter and collect all files that match the inputFileExtension - List matchingFiles = outputFiles.stream() - .filter(file -> file.getFilename().endsWith(finalInputFileExtension)) - .collect(Collectors.toList()); + outputFiles = newOutputFiles; + } - // Check if there are matching files - if (!matchingFiles.isEmpty()) { - // Create a new MultiValueMap for the request body - MultiValueMap body = new LinkedMultiValueMap<>(); + } else { + // Filter and collect all files that match the inputFileExtension + List matchingFiles = + outputFiles.stream() + .filter( + file -> + file.getFilename() + .endsWith(finalInputFileExtension)) + .collect(Collectors.toList()); - // Add all matching files to the body - for (Resource file : matchingFiles) { - body.add("fileInput", file); - } + // Check if there are matching files + if (!matchingFiles.isEmpty()) { + // Create a new MultiValueMap for the request body + MultiValueMap body = new LinkedMultiValueMap<>(); - for(Entry entry : parameters.entrySet()) { - body.add(entry.getKey(), entry.getValue()); - } - - ResponseEntity response = sendWebRequest(url, body); + // Add all matching files to the body + for (Resource file : matchingFiles) { + body.add("fileInput", file); + } - // Handle the response - if (response.getStatusCode().equals(HttpStatus.OK)) { - processOutputFiles(operation, matchingFiles.get(0).getFilename(), response, newOutputFiles); - } else { - // Log error if the response status is not OK - logPrintStream.println("Error in multi-input operation: " + response.getBody()); - hasErrors = true; - } - } else { - logPrintStream.println("No files with extension " + inputFileExtension + " found for multi-input operation " + operation); - hasErrors = true; - } - } - logPrintStream.close(); + for (Entry entry : parameters.entrySet()) { + body.add(entry.getKey(), entry.getValue()); + } - } - if (hasErrors) { - logger.error("Errors occurred during processing. Log: {}", logStream.toString()); - } - return outputFiles; - } + ResponseEntity response = sendWebRequest(url, body); - private ResponseEntity sendWebRequest(String url, MultiValueMap body ){ - RestTemplate restTemplate = new RestTemplate(); - - // Set up headers, including API key + // Handle the response + if (response.getStatusCode().equals(HttpStatus.OK)) { + processOutputFiles( + operation, + matchingFiles.get(0).getFilename(), + response, + newOutputFiles); + } else { + // Log error if the response status is not OK + logPrintStream.println( + "Error in multi-input operation: " + response.getBody()); + hasErrors = true; + } + } else { + logPrintStream.println( + "No files with extension " + + inputFileExtension + + " found for multi-input operation " + + operation); + hasErrors = true; + } + } + logPrintStream.close(); + } + if (hasErrors) { + logger.error("Errors occurred during processing. Log: {}", logStream.toString()); + } + return outputFiles; + } + + private ResponseEntity sendWebRequest(String url, MultiValueMap body) { + RestTemplate restTemplate = new RestTemplate(); + + // Set up headers, including API key HttpHeaders headers = new HttpHeaders(); String apiKey = getApiKeyForUser(); headers.add("X-API-Key", apiKey); @@ -199,134 +206,141 @@ public class PipelineProcessor { // Make the request to the REST endpoint return restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class); - } - - private List processOutputFiles(String operation, String fileName, ResponseEntity response, List newOutputFiles) throws IOException{ - // Define filename - String newFilename; - if ("auto-rename".equals(operation)) { - // If the operation is "auto-rename", generate a new filename. - // This is a simple example of generating a filename using current timestamp. - // Modify as per your needs. - newFilename = "file_" + System.currentTimeMillis(); - } else { - // Otherwise, keep the original filename. - newFilename = fileName; - } + } - // Check if the response body is a zip file - if (isZip(response.getBody())) { - // Unzip the file and add all the files to the new output files - newOutputFiles.addAll(unzip(response.getBody())); - } else { - Resource outputResource = new ByteArrayResource(response.getBody()) { - @Override - public String getFilename() { - return newFilename; - } - }; - newOutputFiles.add(outputResource); - } - - return newOutputFiles; - - } - List generateInputFiles(File[] files) throws Exception { - if (files == null || files.length == 0) { - logger.info("No files"); - return null; - } + private List processOutputFiles( + String operation, + String fileName, + ResponseEntity response, + List newOutputFiles) + throws IOException { + // Define filename + String newFilename; + if ("auto-rename".equals(operation)) { + // If the operation is "auto-rename", generate a new filename. + // This is a simple example of generating a filename using current timestamp. + // Modify as per your needs. + newFilename = "file_" + System.currentTimeMillis(); + } else { + // Otherwise, keep the original filename. + newFilename = fileName; + } - - List outputFiles = new ArrayList<>(); + // Check if the response body is a zip file + if (isZip(response.getBody())) { + // Unzip the file and add all the files to the new output files + newOutputFiles.addAll(unzip(response.getBody())); + } else { + Resource outputResource = + new ByteArrayResource(response.getBody()) { + @Override + public String getFilename() { + return newFilename; + } + }; + newOutputFiles.add(outputResource); + } - for (File file : files) { - Path path = Paths.get(file.getAbsolutePath()); - logger.info("Reading file: " + path); // debug statement + return newOutputFiles; + } - if (Files.exists(path)) { - Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) { - @Override - public String getFilename() { - return file.getName(); - } - }; - outputFiles.add(fileResource); - } else { - logger.info("File not found: " + path); - } - } - logger.info("Files successfully loaded. Starting processing..."); - return outputFiles; - } + List generateInputFiles(File[] files) throws Exception { + if (files == null || files.length == 0) { + logger.info("No files"); + return null; + } - List generateInputFiles(MultipartFile[] files) throws Exception { - if (files == null || files.length == 0) { - logger.info("No files"); - return null; - } + List outputFiles = new ArrayList<>(); - List outputFiles = new ArrayList<>(); + for (File file : files) { + Path path = Paths.get(file.getAbsolutePath()); + logger.info("Reading file: " + path); // debug statement - for (MultipartFile file : files) { - Resource fileResource = new ByteArrayResource(file.getBytes()) { - @Override - public String getFilename() { - return file.getOriginalFilename(); - } - }; - outputFiles.add(fileResource); - } - logger.info("Files successfully loaded. Starting processing..."); - return outputFiles; - } + if (Files.exists(path)) { + Resource fileResource = + new ByteArrayResource(Files.readAllBytes(path)) { + @Override + public String getFilename() { + return file.getName(); + } + }; + outputFiles.add(fileResource); + } else { + logger.info("File not found: " + path); + } + } + logger.info("Files successfully loaded. Starting processing..."); + return outputFiles; + } - private boolean isZip(byte[] data) { - if (data == null || data.length < 4) { - return false; - } + List generateInputFiles(MultipartFile[] files) throws Exception { + if (files == null || files.length == 0) { + logger.info("No files"); + return null; + } - // 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; - } + List outputFiles = new ArrayList<>(); - private List unzip(byte[] data) throws IOException { - logger.info("Unzipping data of length: {}", data.length); - List unzippedFiles = new ArrayList<>(); + for (MultipartFile file : files) { + Resource fileResource = + new ByteArrayResource(file.getBytes()) { + @Override + public String getFilename() { + return file.getOriginalFilename(); + } + }; + outputFiles.add(fileResource); + } + logger.info("Files successfully loaded. Starting processing..."); + return outputFiles; + } - try (ByteArrayInputStream bais = new ByteArrayInputStream(data); - ZipInputStream zis = new ZipInputStream(bais)) { + private boolean isZip(byte[] data) { + if (data == null || data.length < 4) { + return false; + } - ZipEntry entry; - while ((entry = zis.getNextEntry()) != null) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int count; + // 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; + } - while ((count = zis.read(buffer)) != -1) { - baos.write(buffer, 0, count); - } + private List unzip(byte[] data) throws IOException { + logger.info("Unzipping data of length: {}", data.length); + List unzippedFiles = new ArrayList<>(); - final String filename = entry.getName(); - Resource fileResource = new ByteArrayResource(baos.toByteArray()) { - @Override - public String getFilename() { - return filename; - } - }; + try (ByteArrayInputStream bais = new ByteArrayInputStream(data); + ZipInputStream zis = new ZipInputStream(bais)) { - // 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); - } - } - } + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int count; - logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size()); - return unzippedFiles; - } + 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/pipeline/UserServiceInterface.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/UserServiceInterface.java index f1203be8..1a60441e 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/UserServiceInterface.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/UserServiceInterface.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.controller.api.pipeline; + public interface UserServiceInterface { String getApiKeyForUser(String username); } diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java index b0e5f2aa..8990c789 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java @@ -53,6 +53,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest; import stirling.software.SPDF.utils.WebResponseUtils; @@ -61,198 +62,228 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "Security", description = "Security APIs") public class CertSignController { - private static final Logger logger = LoggerFactory.getLogger(CertSignController.class); + private static final Logger logger = LoggerFactory.getLogger(CertSignController.class); - static { - Security.addProvider(new BouncyCastleProvider()); - } + static { + Security.addProvider(new BouncyCastleProvider()); + } - @PostMapping(consumes = "multipart/form-data", value = "/cert-sign") - @Operation(summary = "Sign PDF with a Digital Certificate", description = "This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:MF-SISO") - public ResponseEntity signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) throws Exception { - MultipartFile pdf = request.getFileInput(); - String certType = request.getCertType(); - MultipartFile privateKeyFile = request.getPrivateKeyFile(); - MultipartFile certFile = request.getCertFile(); - MultipartFile p12File = request.getP12File(); - String password = request.getPassword(); - Boolean showSignature = request.isShowSignature(); - String reason = request.getReason(); - String location = request.getLocation(); - String name = request.getName(); - Integer pageNumber = request.getPageNumber(); + @PostMapping(consumes = "multipart/form-data", value = "/cert-sign") + @Operation( + summary = "Sign PDF with a Digital Certificate", + description = + "This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:MF-SISO") + public ResponseEntity signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) + throws Exception { + MultipartFile pdf = request.getFileInput(); + String certType = request.getCertType(); + MultipartFile privateKeyFile = request.getPrivateKeyFile(); + MultipartFile certFile = request.getCertFile(); + MultipartFile p12File = request.getP12File(); + String password = request.getPassword(); + Boolean showSignature = request.isShowSignature(); + String reason = request.getReason(); + String location = request.getLocation(); + String name = request.getName(); + Integer pageNumber = request.getPageNumber(); - PrivateKey privateKey = null; - X509Certificate cert = null; + PrivateKey privateKey = null; + X509Certificate cert = null; - if (certType != null) { - logger.info("Cert type provided: {}", certType); - switch (certType) { - case "PKCS12": - if (p12File != null) { - KeyStore ks = KeyStore.getInstance("PKCS12"); - ks.load(new ByteArrayInputStream(p12File.getBytes()), password.toCharArray()); - String alias = ks.aliases().nextElement(); - if (!ks.isKeyEntry(alias)) { - throw new IllegalArgumentException("The provided PKCS12 file does not contain a private key."); - } - privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray()); - cert = (X509Certificate) ks.getCertificate(alias); - } - break; - case "PEM": - if (privateKeyFile != null && certFile != null) { - // Load private key - KeyFactory keyFactory = KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME); - if (isPEM(privateKeyFile.getBytes())) { - privateKey = keyFactory - .generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes()))); - } else { - privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes())); - } + if (certType != null) { + logger.info("Cert type provided: {}", certType); + switch (certType) { + case "PKCS12": + if (p12File != null) { + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load( + new ByteArrayInputStream(p12File.getBytes()), + password.toCharArray()); + String alias = ks.aliases().nextElement(); + if (!ks.isKeyEntry(alias)) { + throw new IllegalArgumentException( + "The provided PKCS12 file does not contain a private key."); + } + privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray()); + cert = (X509Certificate) ks.getCertificate(alias); + } + break; + case "PEM": + if (privateKeyFile != null && certFile != null) { + // Load private key + KeyFactory keyFactory = + KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME); + if (isPEM(privateKeyFile.getBytes())) { + privateKey = + keyFactory.generatePrivate( + new PKCS8EncodedKeySpec( + parsePEM(privateKeyFile.getBytes()))); + } else { + privateKey = + keyFactory.generatePrivate( + new PKCS8EncodedKeySpec(privateKeyFile.getBytes())); + } - // Load certificate - CertificateFactory certFactory = CertificateFactory.getInstance("X.509", - BouncyCastleProvider.PROVIDER_NAME); - if (isPEM(certFile.getBytes())) { - cert = (X509Certificate) certFactory - .generateCertificate(new ByteArrayInputStream(parsePEM(certFile.getBytes()))); - } else { - cert = (X509Certificate) certFactory - .generateCertificate(new ByteArrayInputStream(certFile.getBytes())); - } - } - break; - } - } - PDSignature signature = new PDSignature(); - signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); // default filter - signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_SHA1); - signature.setName(name); - signature.setLocation(location); - signature.setReason(reason); - signature.setSignDate(Calendar.getInstance()); - - // Load the PDF - try (PDDocument document = PDDocument.load(pdf.getBytes())) { - logger.info("Successfully loaded the provided PDF"); - SignatureOptions signatureOptions = new SignatureOptions(); + // Load certificate + CertificateFactory certFactory = + CertificateFactory.getInstance( + "X.509", BouncyCastleProvider.PROVIDER_NAME); + if (isPEM(certFile.getBytes())) { + cert = + (X509Certificate) + certFactory.generateCertificate( + new ByteArrayInputStream( + parsePEM(certFile.getBytes()))); + } else { + cert = + (X509Certificate) + certFactory.generateCertificate( + new ByteArrayInputStream(certFile.getBytes())); + } + } + break; + } + } + PDSignature signature = new PDSignature(); + signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); // default filter + signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_SHA1); + signature.setName(name); + signature.setLocation(location); + signature.setReason(reason); + signature.setSignDate(Calendar.getInstance()); - // If you want to show the signature + // Load the PDF + try (PDDocument document = PDDocument.load(pdf.getBytes())) { + logger.info("Successfully loaded the provided PDF"); + SignatureOptions signatureOptions = new SignatureOptions(); - // ATTEMPT 2 - if (showSignature != null && showSignature) { - PDPage page = document.getPage(pageNumber - 1); + // If you want to show the signature - PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); - if (acroForm == null) { - acroForm = new PDAcroForm(document); - document.getDocumentCatalog().setAcroForm(acroForm); - } + // ATTEMPT 2 + if (showSignature != null && showSignature) { + PDPage page = document.getPage(pageNumber - 1); - // Create a new signature field and widget + PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); + if (acroForm == null) { + acroForm = new PDAcroForm(document); + document.getDocumentCatalog().setAcroForm(acroForm); + } - PDSignatureField signatureField = new PDSignatureField(acroForm); - PDAnnotationWidget widget = signatureField.getWidgets().get(0); - PDRectangle rect = new PDRectangle(100, 100, 200, 50); // Define the rectangle size here - widget.setRectangle(rect); - page.getAnnotations().add(widget); + // Create a new signature field and widget -// Set the appearance for the signature field - PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary(); - PDAppearanceStream appearanceStream = new PDAppearanceStream(document); - appearanceStream.setResources(new PDResources()); - appearanceStream.setBBox(rect); - appearanceDict.setNormalAppearance(appearanceStream); - widget.setAppearance(appearanceDict); + PDSignatureField signatureField = new PDSignatureField(acroForm); + PDAnnotationWidget widget = signatureField.getWidgets().get(0); + PDRectangle rect = + new PDRectangle(100, 100, 200, 50); // Define the rectangle size here + widget.setRectangle(rect); + page.getAnnotations().add(widget); - try (PDPageContentStream contentStream = new PDPageContentStream(document, appearanceStream)) { - contentStream.beginText(); - contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12); - contentStream.newLineAtOffset(110, 130); - contentStream.showText("Digitally signed by: " + (name != null ? name : "Unknown")); - contentStream.newLineAtOffset(0, -15); - contentStream.showText("Date: " + new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z").format(new Date())); - contentStream.newLineAtOffset(0, -15); - if (reason != null && !reason.isEmpty()) { - contentStream.showText("Reason: " + reason); - contentStream.newLineAtOffset(0, -15); - } - if (location != null && !location.isEmpty()) { - contentStream.showText("Location: " + location); - contentStream.newLineAtOffset(0, -15); - } - contentStream.endText(); - } + // Set the appearance for the signature field + PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary(); + PDAppearanceStream appearanceStream = new PDAppearanceStream(document); + appearanceStream.setResources(new PDResources()); + appearanceStream.setBBox(rect); + appearanceDict.setNormalAppearance(appearanceStream); + widget.setAppearance(appearanceDict); - // Add the widget annotation to the page - page.getAnnotations().add(widget); + try (PDPageContentStream contentStream = + new PDPageContentStream(document, appearanceStream)) { + contentStream.beginText(); + contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12); + contentStream.newLineAtOffset(110, 130); + contentStream.showText( + "Digitally signed by: " + (name != null ? name : "Unknown")); + contentStream.newLineAtOffset(0, -15); + contentStream.showText( + "Date: " + + new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z") + .format(new Date())); + contentStream.newLineAtOffset(0, -15); + if (reason != null && !reason.isEmpty()) { + contentStream.showText("Reason: " + reason); + contentStream.newLineAtOffset(0, -15); + } + if (location != null && !location.isEmpty()) { + contentStream.showText("Location: " + location); + contentStream.newLineAtOffset(0, -15); + } + contentStream.endText(); + } - // Add the signature field to the acroform - acroForm.getFields().add(signatureField); + // Add the widget annotation to the page + page.getAnnotations().add(widget); - // Handle multiple signatures by ensuring a unique field name - String baseFieldName = "Signature"; - String signatureFieldName = baseFieldName; - int suffix = 1; - while (acroForm.getField(signatureFieldName) != null) { - suffix++; - signatureFieldName = baseFieldName + suffix; - } - signatureField.setPartialName(signatureFieldName); - } - - document.addSignature(signature, signatureOptions); - logger.info("Signature added to the PDF document"); - // External signing - ExternalSigningSupport externalSigning = document - .saveIncrementalForExternalSigning(new ByteArrayOutputStream()); + // Add the signature field to the acroform + acroForm.getFields().add(signatureField); - byte[] content = IOUtils.toByteArray(externalSigning.getContent()); + // Handle multiple signatures by ensuring a unique field name + String baseFieldName = "Signature"; + String signatureFieldName = baseFieldName; + int suffix = 1; + while (acroForm.getField(signatureFieldName) != null) { + suffix++; + signatureFieldName = baseFieldName + suffix; + } + signatureField.setPartialName(signatureFieldName); + } - // Using BouncyCastle to sign - CMSTypedData cmsData = new CMSProcessableByteArray(content); + document.addSignature(signature, signatureOptions); + logger.info("Signature added to the PDF document"); + // External signing + ExternalSigningSupport externalSigning = + document.saveIncrementalForExternalSigning(new ByteArrayOutputStream()); - CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); - ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA") - .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(privateKey); + byte[] content = IOUtils.toByteArray(externalSigning.getContent()); - gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder( - new JcaDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build()) - .build(signer, cert)); + // Using BouncyCastle to sign + CMSTypedData cmsData = new CMSProcessableByteArray(content); - gen.addCertificates(new JcaCertStore(Collections.singletonList(cert))); - CMSSignedData signedData = gen.generate(cmsData, false); + CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); + ContentSigner signer = + new JcaContentSignerBuilder("SHA256withRSA") + .setProvider(BouncyCastleProvider.PROVIDER_NAME) + .build(privateKey); - byte[] cmsSignature = signedData.getEncoded(); - logger.info("About to sign content using BouncyCastle"); - externalSigning.setSignature(cmsSignature); - logger.info("Signature set successfully"); + gen.addSignerInfoGenerator( + new JcaSignerInfoGeneratorBuilder( + new JcaDigestCalculatorProviderBuilder() + .setProvider(BouncyCastleProvider.PROVIDER_NAME) + .build()) + .build(signer, cert)); - // After setting the signature, return the resultant PDF - try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) { - document.save(signedPdfOutput); - return WebResponseUtils.boasToWebResponse(signedPdfOutput, - pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_signed.pdf"); + gen.addCertificates(new JcaCertStore(Collections.singletonList(cert))); + CMSSignedData signedData = gen.generate(cmsData, false); - } catch (Exception e) { - e.printStackTrace(); - } - } catch (Exception e) { - e.printStackTrace(); - } + byte[] cmsSignature = signedData.getEncoded(); + logger.info("About to sign content using BouncyCastle"); + externalSigning.setSignature(cmsSignature); + logger.info("Signature set successfully"); - return null; - } + // After setting the signature, return the resultant PDF + try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) { + document.save(signedPdfOutput); + return WebResponseUtils.boasToWebResponse( + signedPdfOutput, + pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_signed.pdf"); - private byte[] parsePEM(byte[] content) throws IOException { - PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(content))); - return pemReader.readPemObject().getContent(); - } + } catch (Exception e) { + e.printStackTrace(); + } + } catch (Exception e) { + e.printStackTrace(); + } - private boolean isPEM(byte[] content) { - String contentStr = new String(content); - return contentStr.contains("-----BEGIN") && contentStr.contains("-----END"); - } + return null; + } + private byte[] parsePEM(byte[] content) throws IOException { + PemReader pemReader = + new PemReader(new InputStreamReader(new ByteArrayInputStream(content))); + return pemReader.readPemObject().getContent(); + } + + private boolean isPEM(byte[] content) { + String contentStr = new String(content); + return contentStr.contains("-----BEGIN") && contentStr.contains("-----END"); + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java b/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java index 791dc736..f0bd8438 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java @@ -72,23 +72,22 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/security") @Tag(name = "Security", description = "Security APIs") public class GetInfoOnPDF { - - static ObjectMapper objectMapper = new ObjectMapper(); - @PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf") + static ObjectMapper objectMapper = new ObjectMapper(); + + @PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf") @Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO") - public ResponseEntity getPdfInfo(@ModelAttribute PDFFile request) - throws IOException { - MultipartFile inputFile = request.getFileInput(); - try ( - PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream()); - ) { + public ResponseEntity getPdfInfo(@ModelAttribute PDFFile request) throws IOException { + MultipartFile inputFile = request.getFileInput(); + try (PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream()); ) { ObjectMapper objectMapper = new ObjectMapper(); ObjectNode jsonOutput = objectMapper.createObjectNode(); @@ -100,8 +99,7 @@ public class GetInfoOnPDF { ObjectNode compliancy = objectMapper.createObjectNode(); ObjectNode encryption = objectMapper.createObjectNode(); ObjectNode other = objectMapper.createObjectNode(); - - + metadata.put("Title", info.getTitle()); metadata.put("Author", info.getAuthor()); metadata.put("Subject", info.getSubject()); @@ -111,14 +109,11 @@ public class GetInfoOnPDF { metadata.put("CreationDate", formatDate(info.getCreationDate())); metadata.put("ModificationDate", formatDate(info.getModificationDate())); jsonOutput.set("Metadata", metadata); - - - - + // Total file size of the PDF long fileSizeInBytes = inputFile.getSize(); basicInfo.put("FileSizeInBytes", fileSizeInBytes); - + // Number of words, paragraphs, and images in the entire document String fullText = new PDFTextStripper().getText(pdfBoxDoc); String[] words = fullText.split("\\s+"); @@ -129,8 +124,7 @@ public class GetInfoOnPDF { // Number of characters in the entire document (including spaces and special characters) int charCount = fullText.length(); basicInfo.put("CharacterCount", charCount); - - + // Initialize the flags and types boolean hasCompression = false; String compressionType = "None"; @@ -147,26 +141,21 @@ public class GetInfoOnPDF { } } basicInfo.put("Compression", hasCompression); - if(hasCompression) - basicInfo.put("CompressionType", compressionType); - + if (hasCompression) basicInfo.put("CompressionType", compressionType); + String language = pdfBoxDoc.getDocumentCatalog().getLanguage(); basicInfo.put("Language", language); basicInfo.put("Number of pages", pdfBoxDoc.getNumberOfPages()); - - + PDDocumentCatalog catalog = pdfBoxDoc.getDocumentCatalog(); String pageMode = catalog.getPageMode().name(); - + // Document Information using PDFBox docInfoNode.put("PDF version", pdfBoxDoc.getVersion()); docInfoNode.put("Trapped", info.getTrapped()); - docInfoNode.put("Page Mode", getPageModeDescription(pageMode));; - + docInfoNode.put("Page Mode", getPageModeDescription(pageMode)); + ; - - - PDAcroForm acroForm = pdfBoxDoc.getDocumentCatalog().getAcroForm(); ObjectNode formFieldsNode = objectMapper.createObjectNode(); @@ -177,41 +166,37 @@ public class GetInfoOnPDF { } jsonOutput.set("FormFields", formFieldsNode); - - - - - - //embeed files TODO size - if(catalog.getNames() != null) { - PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles(); - - ArrayNode embeddedFilesArray = objectMapper.createArrayNode(); - if (efTree != null) { - Map efMap = efTree.getNames(); - if (efMap != null) { - for (Map.Entry entry : efMap.entrySet()) { - ObjectNode embeddedFileNode = objectMapper.createObjectNode(); - embeddedFileNode.put("Name", entry.getKey()); - PDEmbeddedFile embeddedFile = entry.getValue().getEmbeddedFile(); - if (embeddedFile != null) { - embeddedFileNode.put("FileSize", embeddedFile.getLength()); // size in bytes - } - embeddedFilesArray.add(embeddedFileNode); - } - } - } - other.set("EmbeddedFiles", embeddedFilesArray); + // embeed files TODO size + if (catalog.getNames() != null) { + PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles(); + + ArrayNode embeddedFilesArray = objectMapper.createArrayNode(); + if (efTree != null) { + Map efMap = efTree.getNames(); + if (efMap != null) { + for (Map.Entry entry : + efMap.entrySet()) { + ObjectNode embeddedFileNode = objectMapper.createObjectNode(); + embeddedFileNode.put("Name", entry.getKey()); + PDEmbeddedFile embeddedFile = entry.getValue().getEmbeddedFile(); + if (embeddedFile != null) { + embeddedFileNode.put( + "FileSize", embeddedFile.getLength()); // size in bytes + } + embeddedFilesArray.add(embeddedFileNode); + } + } + } + other.set("EmbeddedFiles", embeddedFilesArray); } - - - //attachments TODO size + // attachments TODO size ArrayNode attachmentsArray = objectMapper.createArrayNode(); for (PDPage page : pdfBoxDoc.getPages()) { for (PDAnnotation annotation : page.getAnnotations()) { if (annotation instanceof PDAnnotationFileAttachment) { - PDAnnotationFileAttachment fileAttachmentAnnotation = (PDAnnotationFileAttachment) annotation; + PDAnnotationFileAttachment fileAttachmentAnnotation = + (PDAnnotationFileAttachment) annotation; ObjectNode attachmentNode = objectMapper.createObjectNode(); attachmentNode.put("Name", fileAttachmentAnnotation.getAttachmentName()); @@ -223,7 +208,7 @@ public class GetInfoOnPDF { } other.set("Attachments", attachmentsArray); - //Javascript + // Javascript PDDocumentNameDictionary namesDict = catalog.getNames(); ArrayNode javascriptArray = objectMapper.createArrayNode(); @@ -254,9 +239,9 @@ public class GetInfoOnPDF { } other.set("JavaScript", javascriptArray); - - //TODO size - PDOptionalContentProperties ocProperties = pdfBoxDoc.getDocumentCatalog().getOCProperties(); + // TODO size + PDOptionalContentProperties ocProperties = + pdfBoxDoc.getDocumentCatalog().getOCProperties(); ArrayNode layersArray = objectMapper.createArrayNode(); if (ocProperties != null) { @@ -268,34 +253,38 @@ public class GetInfoOnPDF { } other.set("Layers", layersArray); - - //TODO Security - + // TODO Security - - - PDStructureTreeRoot structureTreeRoot = pdfBoxDoc.getDocumentCatalog().getStructureTreeRoot(); + PDStructureTreeRoot structureTreeRoot = + pdfBoxDoc.getDocumentCatalog().getStructureTreeRoot(); ArrayNode structureTreeArray; - try { - if(structureTreeRoot != null) { - structureTreeArray = exploreStructureTree(structureTreeRoot.getKids()); - other.set("StructureTree", structureTreeArray); - } - } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - + try { + if (structureTreeRoot != null) { + structureTreeArray = exploreStructureTree(structureTreeRoot.getKids()); + other.set("StructureTree", structureTreeArray); + } + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A"); boolean isPdfXCompliant = checkForStandard(pdfBoxDoc, "PDF/X"); boolean isPdfECompliant = checkForStandard(pdfBoxDoc, "PDF/E"); boolean isPdfVTCompliant = checkForStandard(pdfBoxDoc, "PDF/VT"); boolean isPdfUACompliant = checkForStandard(pdfBoxDoc, "PDF/UA"); - boolean isPdfBCompliant = checkForStandard(pdfBoxDoc, "PDF/B"); // If you want to check for PDF/Broadcast, though this isn't an official ISO standard. - boolean isPdfSECCompliant = checkForStandard(pdfBoxDoc, "PDF/SEC"); // This might not be effective since PDF/SEC was under development in 2021. - + boolean isPdfBCompliant = + checkForStandard( + pdfBoxDoc, + "PDF/B"); // If you want to check for PDF/Broadcast, though this isn't + // an official ISO standard. + boolean isPdfSECCompliant = + checkForStandard( + pdfBoxDoc, + "PDF/SEC"); // This might not be effective since PDF/SEC was under + // development in 2021. + compliancy.put("IsPDF/ACompliant", isPdfACompliant); compliancy.put("IsPDF/XCompliant", isPdfXCompliant); compliancy.put("IsPDF/ECompliant", isPdfECompliant); @@ -304,10 +293,6 @@ public class GetInfoOnPDF { compliancy.put("IsPDF/BCompliant", isPdfBCompliant); compliancy.put("IsPDF/SECCompliant", isPdfSECCompliant); - - - - PDOutlineNode root = pdfBoxDoc.getDocumentCatalog().getDocumentOutline(); ArrayNode bookmarksArray = objectMapper.createArrayNode(); @@ -318,33 +303,29 @@ public class GetInfoOnPDF { } other.set("Bookmarks/Outline/TOC", bookmarksArray); - - - PDMetadata pdMetadata = pdfBoxDoc.getDocumentCatalog().getMetadata(); - - String xmpString = null; - - if (pdMetadata != null) { - try { - COSInputStream is = pdMetadata.createInputStream(); - DomXmpParser domXmpParser = new DomXmpParser(); - XMPMetadata xmpMeta = domXmpParser.parse(is); - - ByteArrayOutputStream os = new ByteArrayOutputStream(); - new XmpSerializer().serialize(xmpMeta, os, true); - xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8); - } catch (XmpParsingException | IOException e) { - e.printStackTrace(); - } - } - - other.put("XMPMetadata", xmpString); + PDMetadata pdMetadata = pdfBoxDoc.getDocumentCatalog().getMetadata(); + + String xmpString = null; + + if (pdMetadata != null) { + try { + COSInputStream is = pdMetadata.createInputStream(); + DomXmpParser domXmpParser = new DomXmpParser(); + XMPMetadata xmpMeta = domXmpParser.parse(is); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + new XmpSerializer().serialize(xmpMeta, os, true); + xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8); + } catch (XmpParsingException | IOException e) { + e.printStackTrace(); + } + } + + other.put("XMPMetadata", xmpString); - - if (pdfBoxDoc.isEncrypted()) { - encryption.put("IsEncrypted", true); + encryption.put("IsEncrypted", true); // Retrieve encryption details using getEncryption() PDEncryption pdfEncryption = pdfBoxDoc.getEncryption(); @@ -353,31 +334,30 @@ public class GetInfoOnPDF { AccessPermission ap = pdfBoxDoc.getCurrentAccessPermission(); if (ap != null) { ObjectNode permissionsNode = objectMapper.createObjectNode(); - + permissionsNode.put("CanAssembleDocument", ap.canAssembleDocument()); permissionsNode.put("CanExtractContent", ap.canExtractContent()); - permissionsNode.put("CanExtractForAccessibility", ap.canExtractForAccessibility()); + permissionsNode.put( + "CanExtractForAccessibility", ap.canExtractForAccessibility()); permissionsNode.put("CanFillInForm", ap.canFillInForm()); permissionsNode.put("CanModify", ap.canModify()); permissionsNode.put("CanModifyAnnotations", ap.canModifyAnnotations()); permissionsNode.put("CanPrint", ap.canPrint()); permissionsNode.put("CanPrintDegraded", ap.canPrintDegraded()); - encryption.set("Permissions", permissionsNode); // set the node under "Permissions" - } + encryption.set( + "Permissions", permissionsNode); // set the node under "Permissions" + } // Add other encryption-related properties as needed } else { - encryption.put("IsEncrypted", false); + encryption.put("IsEncrypted", false); } - - - ObjectNode pageInfoParent = objectMapper.createObjectNode(); for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) { ObjectNode pageInfo = objectMapper.createObjectNode(); - // Retrieve the page + // Retrieve the page PDPage page = pdfBoxDoc.getPage(pageNum); // Page-level Information @@ -387,20 +367,20 @@ public class GetInfoOnPDF { float height = mediaBox.getHeight(); ObjectNode sizeInfo = objectMapper.createObjectNode(); - + getDimensionInfo(sizeInfo, width, height); - + sizeInfo.put("Standard Page", getPageSize(width, height)); pageInfo.set("Size", sizeInfo); - + pageInfo.put("Rotation", page.getRotation()); pageInfo.put("Page Orientation", getPageOrientation(width, height)); - // Boxes pageInfo.put("MediaBox", mediaBox.toString()); - // Assuming the following boxes are defined for your document; if not, you may get null values. + // Assuming the following boxes are defined for your document; if not, you may get + // null values. PDRectangle cropBox = page.getCropBox(); pageInfo.put("CropBox", cropBox == null ? "Undefined" : cropBox.toString()); @@ -416,13 +396,13 @@ public class GetInfoOnPDF { // Content Extraction PDFTextStripper textStripper = new PDFTextStripper(); textStripper.setStartPage(pageNum + 1); - textStripper.setEndPage(pageNum +1); + textStripper.setEndPage(pageNum + 1); String pageText = textStripper.getText(pdfBoxDoc); - + pageInfo.put("Text Characters Count", pageText.length()); // // Annotations - + List annotations = page.getAnnotations(); int subtypeCount = 0; @@ -430,10 +410,10 @@ public class GetInfoOnPDF { for (PDAnnotation annotation : annotations) { if (annotation.getSubtype() != null) { - subtypeCount++; // Increase subtype count + subtypeCount++; // Increase subtype count } if (annotation.getContents() != null) { - contentsCount++; // Increase contents count + contentsCount++; // Increase contents count } } @@ -442,26 +422,25 @@ public class GetInfoOnPDF { annotationsObject.put("SubtypeCount", subtypeCount); annotationsObject.put("ContentsCount", contentsCount); pageInfo.set("Annotations", annotationsObject); - - - + // Images (simplified) // This part is non-trivial as images can be embedded in multiple ways in a PDF. // Here is a basic structure to recognize image XObjects on a page. ArrayNode imagesArray = objectMapper.createArrayNode(); PDResources resources = page.getResources(); - for (COSName name : resources.getXObjectNames()) { PDXObject xObject = resources.getXObject(name); if (xObject instanceof PDImageXObject) { PDImageXObject image = (PDImageXObject) xObject; - + ObjectNode imageNode = objectMapper.createObjectNode(); imageNode.put("Width", image.getWidth()); imageNode.put("Height", image.getHeight()); - if(image.getMetadata() != null && image.getMetadata().getFile() != null && image.getMetadata().getFile().getFile() != null) { - imageNode.put("Name", image.getMetadata().getFile().getFile()); + if (image.getMetadata() != null + && image.getMetadata().getFile() != null + && image.getMetadata().getFile().getFile() != null) { + imageNode.put("Name", image.getMetadata().getFile().getFile()); } if (image.getColorSpace() != null) { imageNode.put("ColorSpace", image.getColorSpace().getName()); @@ -472,10 +451,9 @@ public class GetInfoOnPDF { } pageInfo.set("Images", imagesArray); - // Links ArrayNode linksArray = objectMapper.createArrayNode(); - Set uniqueURIs = new HashSet<>(); // To store unique URIs + Set uniqueURIs = new HashSet<>(); // To store unique URIs for (PDAnnotation annotation : annotations) { if (annotation instanceof PDAnnotationLink) { @@ -483,7 +461,7 @@ public class GetInfoOnPDF { if (linkAnnotation.getAction() instanceof PDActionURI) { PDActionURI uriAction = (PDActionURI) linkAnnotation.getAction(); String uri = uriAction.getURI(); - uniqueURIs.add(uri); // Add to set to ensure uniqueness + uniqueURIs.add(uri); // Add to set to ensure uniqueness } } } @@ -495,8 +473,7 @@ public class GetInfoOnPDF { linksArray.add(linkNode); } pageInfo.set("Links", linksArray); - - + // Fonts ArrayNode fontsArray = objectMapper.createArrayNode(); Map uniqueFontsMap = new HashMap<>(); @@ -526,13 +503,13 @@ public class GetInfoOnPDF { fontNode.put("IsNonsymbolic", (flags & 32) != 0); fontNode.put("FontFamily", fontDescriptor.getFontFamily()); - // Font stretch and BBox are not directly available in PDFBox's API, so these are omitted for simplicity + // Font stretch and BBox are not directly available in PDFBox's API, so + // these are omitted for simplicity fontNode.put("FontWeight", fontDescriptor.getFontWeight()); } - // Create a unique key for this font node based on its attributes - String uniqueKey = fontNode.toString(); + String uniqueKey = fontNode.toString(); // Increment count if this font exists, or initialize it if new if (uniqueFontsMap.containsKey(uniqueKey)) { @@ -551,17 +528,7 @@ public class GetInfoOnPDF { } pageInfo.set("Fonts", fontsArray); - - - - - - - - - - - + // Access resources dictionary ArrayNode colorSpacesArray = objectMapper.createArrayNode(); @@ -572,7 +539,7 @@ public class GetInfoOnPDF { PDICCBased iccBased = (PDICCBased) colorSpace; PDStream iccData = iccBased.getPDStream(); byte[] iccBytes = iccData.toByteArray(); - + // TODO: Further decode and analyze the ICC data if needed ObjectNode iccProfileNode = objectMapper.createObjectNode(); iccProfileNode.put("ICC Profile Length", iccBytes.length); @@ -580,14 +547,14 @@ public class GetInfoOnPDF { } } pageInfo.set("Color Spaces & ICC Profiles", colorSpacesArray); - // Other XObjects - Map xObjectCountMap = new HashMap<>(); // To store the count for each type + Map xObjectCountMap = + new HashMap<>(); // To store the count for each type for (COSName name : resources.getXObjectNames()) { PDXObject xObject = resources.getXObject(name); String xObjectType; - + if (xObject instanceof PDImageXObject) { xObjectType = "Image"; } else if (xObject instanceof PDFormXObject) { @@ -597,7 +564,8 @@ public class GetInfoOnPDF { } // Increment the count for this type in the map - xObjectCountMap.put(xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1); + xObjectCountMap.put( + xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1); } // Add the count map to pageInfo (or wherever you want to store it) @@ -606,14 +574,11 @@ public class GetInfoOnPDF { xObjectCountNode.put(entry.getKey(), entry.getValue()); } pageInfo.set("XObjectCounts", xObjectCountNode); - - - ArrayNode multimediaArray = objectMapper.createArrayNode(); for (PDAnnotation annotation : annotations) { - if ("RichMedia".equals(annotation.getSubtype())) { + if ("RichMedia".equals(annotation.getSubtype())) { ObjectNode multimediaNode = objectMapper.createObjectNode(); // Extract details from the annotation as needed multimediaArray.add(multimediaNode); @@ -622,32 +587,29 @@ public class GetInfoOnPDF { pageInfo.set("Multimedia", multimediaArray); - - - pageInfoParent.set("Page " + (pageNum+1), pageInfo); + pageInfoParent.set("Page " + (pageNum + 1), pageInfo); } - jsonOutput.set("BasicInfo", basicInfo); jsonOutput.set("DocumentInfo", docInfoNode); jsonOutput.set("Compliancy", compliancy); jsonOutput.set("Encryption", encryption); jsonOutput.set("Other", other); jsonOutput.set("PerPageInfo", pageInfoParent); - - - + // Save JSON to file - String jsonString = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonOutput); - - - - return WebResponseUtils.bytesToWebResponse(jsonString.getBytes(StandardCharsets.UTF_8), "response.json", MediaType.APPLICATION_JSON); - + String jsonString = + objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonOutput); + + return WebResponseUtils.bytesToWebResponse( + jsonString.getBytes(StandardCharsets.UTF_8), + "response.json", + MediaType.APPLICATION_JSON); + } catch (Exception e) { e.printStackTrace(); } - return null; + return null; } private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) { @@ -665,7 +627,7 @@ public class GetInfoOnPDF { } } - public String getPageOrientation(double width, double height) { + public String getPageOrientation(double width, double height) { if (width > height) { return "Landscape"; } else if (height > width) { @@ -674,6 +636,7 @@ public class GetInfoOnPDF { return "Square"; } } + public String getPageSize(float width, float height) { // Define standard page sizes Map standardSizes = new HashMap<>(); @@ -696,21 +659,22 @@ public class GetInfoOnPDF { return "Custom"; } - private boolean isCloseToSize(float width, float height, float standardWidth, float standardHeight) { + private boolean isCloseToSize( + float width, float height, float standardWidth, float standardHeight) { float tolerance = 1.0f; // You can adjust the tolerance as needed - return Math.abs(width - standardWidth) <= tolerance && Math.abs(height - standardHeight) <= tolerance; + return Math.abs(width - standardWidth) <= tolerance + && Math.abs(height - standardHeight) <= tolerance; } - - public ObjectNode getDimensionInfo(ObjectNode dimensionInfo, float width, float height) { + public ObjectNode getDimensionInfo(ObjectNode dimensionInfo, float width, float height) { float ppi = 72; // Points Per Inch - + float widthInInches = width / ppi; float heightInInches = height / ppi; - + float widthInCm = widthInInches * 2.54f; float heightInCm = heightInInches * 2.54f; - + dimensionInfo.put("Width (px)", String.format("%.2f", width)); dimensionInfo.put("Height (px)", String.format("%.2f", height)); dimensionInfo.put("Width (in)", String.format("%.2f", widthInInches)); @@ -720,33 +684,33 @@ public class GetInfoOnPDF { return dimensionInfo; } + public static boolean checkForStandard(PDDocument document, String standardKeyword) { + // Check XMP Metadata + try { + PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata(); + if (pdMetadata != null) { + COSInputStream metaStream = pdMetadata.createInputStream(); + DomXmpParser domXmpParser = new DomXmpParser(); + XMPMetadata xmpMeta = domXmpParser.parse(metaStream); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new XmpSerializer().serialize(xmpMeta, baos, true); + String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8); -public static boolean checkForStandard(PDDocument document, String standardKeyword) { - // Check XMP Metadata - try { - PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata(); - if (pdMetadata != null) { - COSInputStream metaStream = pdMetadata.createInputStream(); - DomXmpParser domXmpParser = new DomXmpParser(); - XMPMetadata xmpMeta = domXmpParser.parse(metaStream); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - new XmpSerializer().serialize(xmpMeta, baos, true); - String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8); - - if (xmpString.contains(standardKeyword)) { - return true; + if (xmpString.contains(standardKeyword)) { + return true; + } } + } catch ( + Exception + e) { // Catching general exception for brevity, ideally you'd catch specific + // exceptions. + e.printStackTrace(); } - } catch (Exception e) { // Catching general exception for brevity, ideally you'd catch specific exceptions. - e.printStackTrace(); - } - - return false; -} - + return false; + } + public ArrayNode exploreStructureTree(List nodes) { ArrayNode elementsArray = objectMapper.createArrayNode(); if (nodes != null) { @@ -773,7 +737,6 @@ public static boolean checkForStandard(PDDocument document, String standardKeywo return elementsArray; } - public String getContent(PDStructureElement structureElement) { StringBuilder contentBuilder = new StringBuilder(); @@ -790,8 +753,7 @@ public static boolean checkForStandard(PDDocument document, String standardKeywo return contentBuilder.toString(); } - - + private String formatDate(Calendar calendar) { if (calendar != null) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java b/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java index 639b6973..4c3a9517 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java @@ -16,9 +16,11 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.security.AddPasswordRequest; import stirling.software.SPDF.model.api.security.PDFPasswordRequest; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/security") @Tag(name = "Security", description = "Security APIs") @@ -26,29 +28,31 @@ public class PasswordController { private static final Logger logger = LoggerFactory.getLogger(PasswordController.class); - @PostMapping(consumes = "multipart/form-data", value = "/remove-password") @Operation( - summary = "Remove password from a PDF file", - description = "This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO" - ) - public ResponseEntity removePassword(@ModelAttribute PDFPasswordRequest request) throws IOException { + summary = "Remove password from a PDF file", + description = + "This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO") + public ResponseEntity removePassword(@ModelAttribute PDFPasswordRequest request) + throws IOException { MultipartFile fileInput = request.getFileInput(); String password = request.getPassword(); - - - + PDDocument document = PDDocument.load(fileInput.getBytes(), password); document.setAllSecurityToBeRemoved(true); - return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_password_removed.pdf"); + return WebResponseUtils.pdfDocToWebResponse( + document, + fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_password_removed.pdf"); } @PostMapping(consumes = "multipart/form-data", value = "/add-password") @Operation( - summary = "Add password to a PDF file", - description = "This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF" - ) - public ResponseEntity addPassword(@ModelAttribute AddPasswordRequest request) throws IOException { + summary = "Add password to a PDF file", + description = + "This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF") + public ResponseEntity addPassword(@ModelAttribute AddPasswordRequest request) + throws IOException { MultipartFile fileInput = request.getFileInput(); String ownerPassword = request.getOwnerPassword(); String password = request.getPassword(); @@ -74,16 +78,19 @@ public class PasswordController { ap.setCanPrintFaithful(!canPrintFaithful); StandardProtectionPolicy spp = new StandardProtectionPolicy(ownerPassword, password, ap); - if(!"".equals(ownerPassword) || !"".equals(password)) { - spp.setEncryptionKeyLength(keyLength); + if (!"".equals(ownerPassword) || !"".equals(password)) { + spp.setEncryptionKeyLength(keyLength); } spp.setPermissions(ap); document.protect(spp); - if("".equals(ownerPassword) && "".equals(password)) - return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_permissions.pdf"); - return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf"); + if ("".equals(ownerPassword) && "".equals(password)) + return WebResponseUtils.pdfDocToWebResponse( + document, + fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_permissions.pdf"); + return WebResponseUtils.pdfDocToWebResponse( + document, + fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf"); } - - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java b/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java index 825544dc..79d15065 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java @@ -26,10 +26,12 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.PDFText; import stirling.software.SPDF.model.api.security.RedactPdfRequest; import stirling.software.SPDF.pdf.TextFinder; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/security") @Tag(name = "Security", description = "Security APIs") @@ -37,11 +39,13 @@ public class RedactController { private static final Logger logger = LoggerFactory.getLogger(RedactController.class); - @PostMapping(value = "/auto-redact", consumes = "multipart/form-data") - @Operation(summary = "Redacts listOfText in a PDF document", - description = "This operation takes an input PDF file and redacts the provided listOfText. Input:PDF, Output:PDF, Type:SISO") - public ResponseEntity redactPdf(@ModelAttribute RedactPdfRequest request) throws Exception { + @Operation( + summary = "Redacts listOfText in a PDF document", + description = + "This operation takes an input PDF file and redacts the provided listOfText. Input:PDF, Output:PDF, Type:SISO") + public ResponseEntity redactPdf(@ModelAttribute RedactPdfRequest request) + throws Exception { MultipartFile file = request.getFileInput(); String listOfTextString = request.getListOfText(); boolean useRegex = request.isUseRegex(); @@ -49,15 +53,15 @@ public class RedactController { String colorString = request.getRedactColor(); float customPadding = request.getCustomPadding(); boolean convertPDFToImage = request.isConvertPDFToImage(); - - System.out.println(listOfTextString); - String[] listOfText = listOfTextString.split("\n"); + + System.out.println(listOfTextString); + String[] listOfText = listOfTextString.split("\n"); byte[] bytes = file.getBytes(); PDDocument document = PDDocument.load(new ByteArrayInputStream(bytes)); - + Color redactColor; try { - if (!colorString.startsWith("#")) { + if (!colorString.startsWith("#")) { colorString = "#" + colorString; } redactColor = Color.decode(colorString); @@ -66,18 +70,14 @@ public class RedactController { redactColor = Color.BLACK; } - - for (String text : listOfText) { - text = text.trim(); - System.out.println(text); - TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool); + text = text.trim(); + System.out.println(text); + TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool); List foundTexts = textFinder.getTextLocations(document); - redactFoundText(document, foundTexts, customPadding,redactColor); + redactFoundText(document, foundTexts, customPadding, redactColor); } - - - + if (convertPDFToImage) { PDDocument imageDocument = new PDDocument(); PDFRenderer pdfRenderer = new PDFRenderer(document); @@ -97,27 +97,33 @@ public class RedactController { ByteArrayOutputStream baos = new ByteArrayOutputStream(); document.save(baos); document.close(); - + byte[] pdfContent = baos.toByteArray(); - return WebResponseUtils.bytesToWebResponse(pdfContent, + return WebResponseUtils.bytesToWebResponse( + pdfContent, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_redacted.pdf"); } - - private void redactFoundText(PDDocument document, List blocks, float customPadding, Color redactColor) throws IOException { + private void redactFoundText( + PDDocument document, List blocks, float customPadding, Color redactColor) + throws IOException { var allPages = document.getDocumentCatalog().getPages(); for (PDFText block : blocks) { var page = allPages.get(block.getPageIndex()); - PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true, true); + PDPageContentStream contentStream = + new PDPageContentStream( + document, page, PDPageContentStream.AppendMode.APPEND, true, true); contentStream.setNonStrokingColor(redactColor); float padding = (block.getY2() - block.getY1()) * 0.3f + customPadding; PDRectangle pageBox = page.getBBox(); - contentStream.addRect(block.getX1(), pageBox.getHeight() - block.getY1() - padding, block.getX2() - block.getX1(), block.getY2() - block.getY1() + 2 * padding); + contentStream.addRect( + block.getX1(), + pageBox.getHeight() - block.getY1() - padding, + block.getX2() - block.getX1(), + block.getY2() - block.getY1() + 2 * padding); contentStream.fill(); contentStream.close(); } } - - } 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 index dab9d1d7..21a33529 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.controller.api.security; + import java.io.IOException; import org.apache.pdfbox.cos.COSDictionary; @@ -28,6 +29,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.security.SanitizePdfRequest; import stirling.software.SPDF.utils.WebResponseUtils; @@ -36,59 +38,68 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "Security", description = "Security APIs") 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(@ModelAttribute SanitizePdfRequest request) throws IOException { - MultipartFile inputFile = request.getFileInput(); - boolean removeJavaScript = request.isRemoveJavaScript(); - boolean removeEmbeddedFiles = request.isRemoveEmbeddedFiles(); - boolean removeMetadata = request.isRemoveMetadata(); - boolean removeLinks = request.isRemoveLinks(); - boolean removeFonts = request.isRemoveFonts(); + @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(@ModelAttribute SanitizePdfRequest request) + throws IOException { + MultipartFile inputFile = request.getFileInput(); + boolean removeJavaScript = request.isRemoveJavaScript(); + boolean removeEmbeddedFiles = request.isRemoveEmbeddedFiles(); + boolean removeMetadata = request.isRemoveMetadata(); + boolean removeLinks = request.isRemoveLinks(); + boolean removeFonts = request.isRemoveFonts(); - try (PDDocument document = PDDocument.load(inputFile.getInputStream())) { - if (removeJavaScript) { - sanitizeJavaScript(document); - } + try (PDDocument document = PDDocument.load(inputFile.getInputStream())) { + if (removeJavaScript) { + sanitizeJavaScript(document); + } - if (removeEmbeddedFiles) { - sanitizeEmbeddedFiles(document); - } + if (removeEmbeddedFiles) { + sanitizeEmbeddedFiles(document); + } - if (removeMetadata) { - sanitizeMetadata(document); - } + if (removeMetadata) { + sanitizeMetadata(document); + } - if (removeLinks) { - sanitizeLinks(document); - } + if (removeLinks) { + sanitizeLinks(document); + } - if (removeFonts) { - sanitizeFonts(document); - } + if (removeFonts) { + sanitizeFonts(document); + } - return WebResponseUtils.pdfDocToWebResponse(document, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_sanitized.pdf"); - } - } - private void sanitizeJavaScript(PDDocument document) throws IOException { - // Get the root dictionary (catalog) of the PDF - PDDocumentCatalog catalog = document.getDocumentCatalog(); + return WebResponseUtils.pdfDocToWebResponse( + document, + inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_sanitized.pdf"); + } + } - // Get the Names dictionary - COSDictionary namesDict = (COSDictionary) catalog.getCOSObject().getDictionaryObject(COSName.NAMES); + private void sanitizeJavaScript(PDDocument document) throws IOException { + // Get the root dictionary (catalog) of the PDF + PDDocumentCatalog catalog = document.getDocumentCatalog(); - if (namesDict != null) { - // Get the JavaScript dictionary - COSDictionary javaScriptDict = (COSDictionary) namesDict.getDictionaryObject(COSName.getPDFName("JavaScript")); + // Get the Names dictionary + COSDictionary namesDict = + (COSDictionary) catalog.getCOSObject().getDictionaryObject(COSName.NAMES); - if (javaScriptDict != null) { - // Remove the JavaScript dictionary - namesDict.removeItem(COSName.getPDFName("JavaScript")); - } - } - - for (PDPage page : document.getPages()) { + if (namesDict != null) { + // Get the JavaScript dictionary + COSDictionary javaScriptDict = + (COSDictionary) namesDict.getDictionaryObject(COSName.getPDFName("JavaScript")); + + if (javaScriptDict != null) { + // Remove the JavaScript dictionary + namesDict.removeItem(COSName.getPDFName("JavaScript")); + } + } + + for (PDPage page : document.getPages()) { for (PDAnnotation annotation : page.getAnnotations()) { if (annotation instanceof PDAnnotationWidget) { PDAnnotationWidget widget = (PDAnnotationWidget) annotation; @@ -96,33 +107,30 @@ public class SanitizeController { if (action instanceof PDActionJavaScript) { widget.setAction(null); } - } - } - PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); - if (acroForm != null) { - for (PDField field : acroForm.getFields()) { - PDFormFieldAdditionalActions actions = field.getActions(); - if(actions != null) { - if (actions.getC() instanceof PDActionJavaScript) { - actions.setC(null); - } - if (actions.getF() instanceof PDActionJavaScript) { - actions.setF(null); - } - if (actions.getK() instanceof PDActionJavaScript) { - actions.setK(null); - } - if (actions.getV() instanceof PDActionJavaScript) { - actions.setV(null); - } - } - } - } - } - } - - - + } + } + PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); + if (acroForm != null) { + for (PDField field : acroForm.getFields()) { + PDFormFieldAdditionalActions actions = field.getActions(); + if (actions != null) { + if (actions.getC() instanceof PDActionJavaScript) { + actions.setC(null); + } + if (actions.getF() instanceof PDActionJavaScript) { + actions.setF(null); + } + if (actions.getK() instanceof PDActionJavaScript) { + actions.setK(null); + } + if (actions.getV() instanceof PDActionJavaScript) { + actions.setV(null); + } + } + } + } + } + } private void sanitizeEmbeddedFiles(PDDocument document) { PDPageTree allPages = document.getPages(); @@ -134,7 +142,6 @@ public class SanitizeController { res.getCOSObject().removeItem(COSName.getPDFName("EmbeddedFiles")); } } - private void sanitizeMetadata(PDDocument document) { PDMetadata metadata = document.getDocumentCatalog().getMetadata(); @@ -143,8 +150,6 @@ public class SanitizeController { } } - - private void sanitizeLinks(PDDocument document) throws IOException { for (PDPage page : document.getPages()) { for (PDAnnotation annotation : page.getAnnotations()) { @@ -163,5 +168,4 @@ public class SanitizeController { page.getResources().getCOSObject().removeItem(COSName.getPDFName("Font")); } } - } 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 b19636cd..daee68bf 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 @@ -30,6 +30,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.security.AddWatermarkRequest; import stirling.software.SPDF.utils.WebResponseUtils; @@ -38,154 +39,198 @@ import stirling.software.SPDF.utils.WebResponseUtils; @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 type (text or image), rotation, opacity, width spacer, and height spacer. Input:PDF Output:PDF Type:SISO") - public ResponseEntity addWatermark(@ModelAttribute AddWatermarkRequest request) throws IOException, Exception { - MultipartFile pdfFile = request.getFileInput(); - String watermarkType = request.getWatermarkType(); - String watermarkText = request.getWatermarkText(); - MultipartFile watermarkImage = request.getWatermarkImage(); - String alphabet = request.getAlphabet(); - float fontSize = request.getFontSize(); - float rotation = request.getRotation(); - float opacity = request.getOpacity(); - int widthSpacer = request.getWidthSpacer(); - int heightSpacer = request.getHeightSpacer(); + @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(@ModelAttribute AddWatermarkRequest request) + throws IOException, Exception { + MultipartFile pdfFile = request.getFileInput(); + String watermarkType = request.getWatermarkType(); + String watermarkText = request.getWatermarkText(); + MultipartFile watermarkImage = request.getWatermarkImage(); + String alphabet = request.getAlphabet(); + float fontSize = request.getFontSize(); + float rotation = request.getRotation(); + float opacity = request.getOpacity(); + int widthSpacer = request.getWidthSpacer(); + int heightSpacer = request.getHeightSpacer(); - // Load the input PDF - PDDocument document = PDDocument.load(pdfFile.getInputStream()); + // Load the input PDF + PDDocument document = PDDocument.load(pdfFile.getInputStream()); - // Create a page in the document - for (PDPage page : document.getPages()) { + // Create a page in the document + for (PDPage page : document.getPages()) { - // Get the page's content stream - PDPageContentStream contentStream = new PDPageContentStream(document, page, - PDPageContentStream.AppendMode.APPEND, true); + // Get the page's content stream + PDPageContentStream contentStream = + new PDPageContentStream( + document, page, PDPageContentStream.AppendMode.APPEND, true); - // Set transparency - PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState(); - graphicsState.setNonStrokingAlphaConstant(opacity); - contentStream.setGraphicsStateParameters(graphicsState); + // Set transparency + PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState(); + graphicsState.setNonStrokingAlphaConstant(opacity); + contentStream.setGraphicsStateParameters(graphicsState); - 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); - } + 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(); - } + // Close the content stream + contentStream.close(); + } - return WebResponseUtils.pdfDocToWebResponse(document, - pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf"); - } + 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; - } + 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("")) { + if (!resourceDir.equals("")) { ClassPathResource classPathResource = new ClassPathResource(resourceDir); String fileExtension = resourceDir.substring(resourceDir.lastIndexOf(".")); File tempFile = File.createTempFile("NotoSansFont", fileExtension); - try (InputStream is = classPathResource.getInputStream(); FileOutputStream os = new FileOutputStream(tempFile)) { + try (InputStream is = classPathResource.getInputStream(); + FileOutputStream os = new FileOutputStream(tempFile)) { IOUtils.copy(is, os); } - + font = PDType0Font.load(document, tempFile); tempFile.deleteOnExit(); } - - 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); + contentStream.setFont(font, fontSize); + contentStream.setNonStrokingColor(Color.LIGHT_GRAY); - // 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(); - } - } - } + // 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); - private void addImageWatermark(PDPageContentStream contentStream, MultipartFile watermarkImage, PDDocument document, PDPage page, float rotation, - int widthSpacer, int heightSpacer, float fontSize) throws IOException { + // 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(); + } + } + } -// Load the watermark image -BufferedImage image = ImageIO.read(watermarkImage.getInputStream()); + private void addImageWatermark( + PDPageContentStream contentStream, + MultipartFile watermarkImage, + PDDocument document, + PDPage page, + float rotation, + int widthSpacer, + int heightSpacer, + float fontSize) + throws IOException { -// Compute width based on original aspect ratio -float aspectRatio = (float) image.getWidth() / (float) image.getHeight(); + // Load the watermark image + BufferedImage image = ImageIO.read(watermarkImage.getInputStream()); -// Desired physical height (in PDF points) -float desiredPhysicalHeight = fontSize ; + // Compute width based on original aspect ratio + float aspectRatio = (float) image.getWidth() / (float) image.getHeight(); -// Desired physical width based on the aspect ratio -float desiredPhysicalWidth = desiredPhysicalHeight * aspectRatio; + // Desired physical height (in PDF points) + float desiredPhysicalHeight = fontSize; -// Convert the BufferedImage to PDImageXObject -PDImageXObject xobject = LosslessFactory.createFromImage(document, image); + // Desired physical width based on the aspect ratio + float desiredPhysicalWidth = desiredPhysicalHeight * aspectRatio; -// 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)); + // Convert the BufferedImage to PDImageXObject + PDImageXObject xobject = LosslessFactory.createFromImage(document, image); -for (int i = 0; i < watermarkRows; i++) { -for (int j = 0; j < watermarkCols; j++) { -float x = j * (desiredPhysicalWidth + widthSpacer); -float y = i * (desiredPhysicalHeight + heightSpacer); + // 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)); -// Save the graphics state -contentStream.saveGraphicsState(); + for (int i = 0; i < watermarkRows; i++) { + for (int j = 0; j < watermarkCols; j++) { + float x = j * (desiredPhysicalWidth + widthSpacer); + float y = i * (desiredPhysicalHeight + heightSpacer); -// 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(); -} - -} - - } + // 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/api/strippers/PDFTableStripper.java b/src/main/java/stirling/software/SPDF/controller/api/strippers/PDFTableStripper.java index 5330c3ad..e2ed3ca0 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/strippers/PDFTableStripper.java +++ b/src/main/java/stirling/software/SPDF/controller/api/strippers/PDFTableStripper.java @@ -24,91 +24,79 @@ import org.apache.pdfbox.text.PDFTextStripperByArea; import org.apache.pdfbox.text.TextPosition; /** + * Class to extract tabular data from a PDF. Works by making a first pass of the page to group all + * nearby text items together, and then inferring a 2D grid from these regions. Each table cell is + * then extracted using a PDFTextStripperByArea object. * - * Class to extract tabular data from a PDF. - * Works by making a first pass of the page to group all nearby text items - * together, and then inferring a 2D grid from these regions. Each table cell - * is then extracted using a PDFTextStripperByArea object. + *

Works best when headers are included in the detected region, to ensure representative text in + * every column. * - * Works best when - * headers are included in the detected region, to ensure representative text - * in every column. - * - * Based upon DrawPrintTextLocations PDFBox example + *

Based upon DrawPrintTextLocations PDFBox example * (https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/org/apache/pdfbox/examples/util/DrawPrintTextLocations.java) * * @author Beldaz */ -public class PDFTableStripper extends PDFTextStripper -{ +public class PDFTableStripper extends PDFTextStripper { /** * This will print the documents data, for each table cell. * * @param args The command line arguments. - * * @throws IOException If there is an error parsing the document. */ /* * Used in methods derived from DrawPrintTextLocations */ private AffineTransform flipAT; + private AffineTransform rotateAT; - /** - * Regions updated by calls to writeString - */ + /** Regions updated by calls to writeString */ private Set boxes; // Border to allow when finding intersections private double dx = 1.0; // This value works for me, feel free to tweak (or add setter) private double dy = 0.000; // Rows of text tend to overlap, so need to extend - /** - * Region in which to find table (otherwise whole page) - */ + /** Region in which to find table (otherwise whole page) */ private Rectangle2D regionArea; - /** - * Number of rows in inferred table - */ - private int nRows=0; + /** Number of rows in inferred table */ + private int nRows = 0; - /** - * Number of columns in inferred table - */ - private int nCols=0; + /** Number of columns in inferred table */ + private int nCols = 0; - /** - * This is the object that does the text extraction - */ + /** This is the object that does the text extraction */ private PDFTextStripperByArea regionStripper; /** * 1D intervals - used for calculateTableRegions() - * @author Beldaz * + * @author Beldaz */ public static class Interval { double start; double end; + public Interval(double start, double end) { - this.start=start; this.end = end; + this.start = start; + this.end = end; } + public void add(Interval col) { - if(col.startend) - end = col.end; + if (col.start < start) start = col.start; + if (col.end > end) end = col.end; } + public static void addTo(Interval x, LinkedList columns) { int p = 0; Iterator it = columns.iterator(); // Find where x should go - while(it.hasNext()) { + while (it.hasNext()) { Interval col = it.next(); - if(x.end>=col.start) { - if(x.start<=col.end) { // overlaps + if (x.end >= col.start) { + if (x.start <= col.end) { // overlaps x.add(col); it.remove(); } @@ -116,30 +104,26 @@ public class PDFTableStripper extends PDFTextStripper } ++p; } - while(it.hasNext()) { + while (it.hasNext()) { Interval col = it.next(); - if(x.start>col.end) - break; + if (x.start > col.end) break; x.add(col); it.remove(); } columns.add(p, x); } - } - /** * Instantiate a new PDFTableStripper object. * * @param document * @throws IOException If there is an error loading the properties. */ - public PDFTableStripper() throws IOException - { + public PDFTableStripper() throws IOException { super.setShouldSeparateByBeads(false); regionStripper = new PDFTextStripperByArea(); - regionStripper.setSortByPosition( true ); + regionStripper.setSortByPosition(true); } /** @@ -147,18 +131,15 @@ public class PDFTableStripper extends PDFTextStripper * * @param rect The rectangle area to retrieve the text from. */ - public void setRegion(Rectangle2D rect ) - { + public void setRegion(Rectangle2D rect) { regionArea = rect; } - public int getRows() - { + public int getRows() { return nRows; } - public int getColumns() - { + public int getColumns() { return nCols; } @@ -167,13 +148,11 @@ public class PDFTableStripper extends PDFTextStripper * * @return The text that was identified in that region. */ - public String getText(int row, int col) - { - return regionStripper.getTextForRegion("el"+col+"x"+row); + public String getText(int row, int col) { + return regionStripper.getTextForRegion("el" + col + "x" + row); } - public void extractTable(PDPage pdPage) throws IOException - { + public void extractTable(PDPage pdPage) throws IOException { setStartPage(getCurrentPageNo()); setEndPage(getCurrentPageNo()); @@ -186,11 +165,9 @@ public class PDFTableStripper extends PDFTextStripper // page may be rotated rotateAT = new AffineTransform(); int rotation = pdPage.getRotation(); - if (rotation != 0) - { + if (rotation != 0) { PDRectangle mediaBox = pdPage.getMediaBox(); - switch (rotation) - { + switch (rotation) { case 90: rotateAT.translate(mediaBox.getHeight(), 0); break; @@ -213,11 +190,12 @@ public class PDFTableStripper extends PDFTextStripper Rectangle2D[][] regions = calculateTableRegions(); -// System.err.println("Drawing " + nCols + "x" + nRows + "="+ nRows*nCols + " regions"); - for(int i=0; i columns = new LinkedList(); LinkedList rows = new LinkedList(); - for(Rectangle2D box: boxes) { + for (Rectangle2D box : boxes) { Interval x = new Interval(box.getMinX(), box.getMaxX()); Interval y = new Interval(box.getMinY(), box.getMaxY()); @@ -249,12 +227,17 @@ public class PDFTableStripper extends PDFTextStripper nRows = rows.size(); nCols = columns.size(); Rectangle2D[][] regions = new Rectangle2D[nCols][nRows]; - int i=0; + int i = 0; // Label regions from top left, rather than the transformed orientation - for(Interval column: columns) { - int j=0; - for(Interval row: rows) { - regions[nCols-i-1][nRows-j-1] = new Rectangle2D.Double(column.start, row.start, column.end - column.start, row.end - row.start); + for (Interval column : columns) { + int j = 0; + for (Interval row : rows) { + regions[nCols - i - 1][nRows - j - 1] = + new Rectangle2D.Double( + column.start, + row.start, + column.end - column.start, + row.end - row.start); ++j; } ++i; @@ -264,18 +247,15 @@ public class PDFTableStripper extends PDFTextStripper } /** - * Register each character's bounding box, updating boxes field to maintain - * a list of all distinct groups of characters. + * Register each character's bounding box, updating boxes field to maintain a list of all + * distinct groups of characters. * - * Overrides the default functionality of PDFTextStripper. - * Most of this is taken from DrawPrintTextLocations.java, with extra steps - * at end of main loop + *

Overrides the default functionality of PDFTextStripper. Most of this is taken from + * DrawPrintTextLocations.java, with extra steps at end of main loop */ @Override - protected void writeString(String string, List textPositions) throws IOException - { - for (TextPosition text : textPositions) - { + protected void writeString(String string, List textPositions) throws IOException { + for (TextPosition text : textPositions) { // glyph space -> user space // note: text.getTextMatrix() is *not* the Text Matrix, it's the Text Rendering Matrix AffineTransform at = text.getTextMatrix().createAffineTransform(); @@ -283,37 +263,35 @@ public class PDFTableStripper extends PDFTextStripper BoundingBox bbox = font.getBoundingBox(); // advance width, bbox height (glyph space) - float xadvance = font.getWidth(text.getCharacterCodes()[0]); // todo: should iterate all chars - Rectangle2D.Float rect = new Rectangle2D.Float(0, bbox.getLowerLeftY(), xadvance, bbox.getHeight()); + float xadvance = + font.getWidth(text.getCharacterCodes()[0]); // todo: should iterate all chars + Rectangle2D.Float rect = + new Rectangle2D.Float(0, bbox.getLowerLeftY(), xadvance, bbox.getHeight()); - if (font instanceof PDType3Font) - { + if (font instanceof PDType3Font) { // bbox and font matrix are unscaled at.concatenate(font.getFontMatrix().createAffineTransform()); - } - else - { + } else { // bbox and font matrix are already scaled to 1000 - at.scale(1/1000f, 1/1000f); + at.scale(1 / 1000f, 1 / 1000f); } Shape s = at.createTransformedShape(rect); s = flipAT.createTransformedShape(s); s = rotateAT.createTransformedShape(s); - // // Merge character's bounding box with boxes field // Rectangle2D bounds = s.getBounds2D(); // Pad sides to detect almost touching boxes Rectangle2D hitbox = bounds.getBounds2D(); - hitbox.add(bounds.getMinX() - dx , bounds.getMinY() - dy); - hitbox.add(bounds.getMaxX() + dx , bounds.getMaxY() + dy); + hitbox.add(bounds.getMinX() - dx, bounds.getMinY() - dy); + hitbox.add(bounds.getMaxX() + dx, bounds.getMaxY() + dy); // Find all overlapping boxes List intersectList = new ArrayList(); - for(Rectangle2D box: boxes) { - if(box.intersects(hitbox)) { + for (Rectangle2D box : boxes) { + if (box.intersects(hitbox)) { intersectList.add(box); } } @@ -321,38 +299,30 @@ public class PDFTableStripper extends PDFTextStripper // Combine all touching boxes and update // (NOTE: Potentially this could leave some overlapping boxes un-merged, // but it's sufficient for now and get's fixed up in calculateTableRegions) - for(Rectangle2D box: intersectList) { + for (Rectangle2D box : intersectList) { bounds.add(box); boxes.remove(box); } boxes.add(bounds); - } - } /** - * This method does nothing in this derived class, because beads and regions are incompatible. Beads are - * ignored when stripping by area. + * This method does nothing in this derived class, because beads and regions are incompatible. + * Beads are ignored when stripping by area. * * @param aShouldSeparateByBeads The new grouping of beads. */ @Override - public final void setShouldSeparateByBeads(boolean aShouldSeparateByBeads) - { - } + public final void setShouldSeparateByBeads(boolean aShouldSeparateByBeads) {} - /** - * Adapted from PDFTextStripperByArea - * {@inheritDoc} - */ + /** Adapted from PDFTextStripperByArea {@inheritDoc} */ @Override - protected void processTextPosition( TextPosition text ) - { - if(regionArea!=null && !regionArea.contains( text.getX(), text.getY() ) ) { + protected void processTextPosition(TextPosition text) { + if (regionArea != null && !regionArea.contains(text.getX(), text.getY())) { // skip character } else { - super.processTextPosition( text ); + super.processTextPosition(text); } } -} \ No newline at end of file +} diff --git a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java index 2c51e7d8..614dd8a0 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.controller.web; + import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -15,138 +16,140 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.oas.annotations.tags.Tag; + import jakarta.servlet.http.HttpServletRequest; import stirling.software.SPDF.model.Authority; import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.User; import stirling.software.SPDF.repository.UserRepository; + @Controller @Tag(name = "Account Security", description = "Account Security APIs") public class AccountWebController { - - @GetMapping("/login") - public String login(HttpServletRequest request, Model model, Authentication authentication) { - if (authentication != null && authentication.isAuthenticated()) { + @GetMapping("/login") + public String login(HttpServletRequest request, Model model, Authentication authentication) { + if (authentication != null && authentication.isAuthenticated()) { return "redirect:/"; } - - if (request.getParameter("error") != null) { - model.addAttribute("error", request.getParameter("error")); - } - if (request.getParameter("logout") != null) { + if (request.getParameter("error") != null) { - model.addAttribute("logoutMessage", "You have been logged out."); - } - - return "login"; - } - @Autowired - private UserRepository userRepository; // Assuming you have a repository for user operations + model.addAttribute("error", request.getParameter("error")); + } + if (request.getParameter("logout") != null) { + model.addAttribute("logoutMessage", "You have been logged out."); + } - @PreAuthorize("hasRole('ROLE_ADMIN')") - @GetMapping("/addUsers") - public String showAddUserForm(Model model, Authentication authentication) { - List allUsers = userRepository.findAll(); - Iterator iterator = allUsers.iterator(); + return "login"; + } - while(iterator.hasNext()) { - User user = iterator.next(); - if(user != null) { - for (Authority authority : user.getAuthorities()) { - if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) { - iterator.remove(); - break; // Break out of the inner loop once the user is removed - } - } - } - } + @Autowired + private UserRepository userRepository; // Assuming you have a repository for user operations - model.addAttribute("users", allUsers); - model.addAttribute("currentUsername", authentication.getName()); - return "addUsers"; - } + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/addUsers") + public String showAddUserForm(Model model, Authentication authentication) { + List allUsers = userRepository.findAll(); + Iterator iterator = allUsers.iterator(); - - @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") - @GetMapping("/account") - public String account(HttpServletRequest request, Model model, Authentication authentication) { - if (authentication == null || !authentication.isAuthenticated()) { + while (iterator.hasNext()) { + User user = iterator.next(); + if (user != null) { + for (Authority authority : user.getAuthorities()) { + if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) { + iterator.remove(); + break; // Break out of the inner loop once the user is removed + } + } + } + } + + model.addAttribute("users", allUsers); + model.addAttribute("currentUsername", authentication.getName()); + return "addUsers"; + } + + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") + @GetMapping("/account") + public String account(HttpServletRequest request, Model model, Authentication authentication) { + if (authentication == null || !authentication.isAuthenticated()) { return "redirect:/"; } - if (authentication != null && authentication.isAuthenticated()) { - Object principal = authentication.getPrincipal(); + if (authentication != null && authentication.isAuthenticated()) { + Object principal = authentication.getPrincipal(); - if (principal instanceof UserDetails) { - // Cast the principal object to UserDetails - UserDetails userDetails = (UserDetails) principal; + if (principal instanceof UserDetails) { + // Cast the principal object to UserDetails + UserDetails userDetails = (UserDetails) principal; - // Retrieve username and other attributes - String username = userDetails.getUsername(); + // Retrieve username and other attributes + String username = userDetails.getUsername(); - // Fetch user details from the database - Optional user = userRepository.findByUsername(username); // Assuming findByUsername method exists - if (!user.isPresent()) { - // Handle error appropriately - return "redirect:/error"; // Example redirection in case of error - } + // Fetch user details from the database + Optional user = + userRepository.findByUsername( + username); // Assuming findByUsername method exists + if (!user.isPresent()) { + // Handle error appropriately + return "redirect:/error"; // Example redirection in case of error + } - // Convert settings map to JSON string - ObjectMapper objectMapper = new ObjectMapper(); - String settingsJson; - try { - settingsJson = objectMapper.writeValueAsString(user.get().getSettings()); - } catch (JsonProcessingException e) { - // Handle JSON conversion error - e.printStackTrace(); - return "redirect:/error"; // Example redirection in case of error - } + // Convert settings map to JSON string + ObjectMapper objectMapper = new ObjectMapper(); + String settingsJson; + try { + settingsJson = objectMapper.writeValueAsString(user.get().getSettings()); + } catch (JsonProcessingException e) { + // Handle JSON conversion error + e.printStackTrace(); + return "redirect:/error"; // Example redirection in case of error + } - // Add attributes to the model - model.addAttribute("username", username); - model.addAttribute("role", user.get().getRolesAsString()); - model.addAttribute("settings", settingsJson); - model.addAttribute("changeCredsFlag", user.get().isFirstLogin()); - } - } else { - return "redirect:/"; - } - return "account"; - } - - - @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") - @GetMapping("/change-creds") - public String changeCreds(HttpServletRequest request, Model model, Authentication authentication) { - if (authentication == null || !authentication.isAuthenticated()) { + // Add attributes to the model + model.addAttribute("username", username); + model.addAttribute("role", user.get().getRolesAsString()); + model.addAttribute("settings", settingsJson); + model.addAttribute("changeCredsFlag", user.get().isFirstLogin()); + } + } else { return "redirect:/"; } - if (authentication != null && authentication.isAuthenticated()) { - Object principal = authentication.getPrincipal(); + return "account"; + } - if (principal instanceof UserDetails) { - // Cast the principal object to UserDetails - UserDetails userDetails = (UserDetails) principal; + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") + @GetMapping("/change-creds") + public String changeCreds( + HttpServletRequest request, Model model, Authentication authentication) { + if (authentication == null || !authentication.isAuthenticated()) { + return "redirect:/"; + } + if (authentication != null && authentication.isAuthenticated()) { + Object principal = authentication.getPrincipal(); - // Retrieve username and other attributes - String username = userDetails.getUsername(); + if (principal instanceof UserDetails) { + // Cast the principal object to UserDetails + UserDetails userDetails = (UserDetails) principal; - // Fetch user details from the database - Optional user = userRepository.findByUsername(username); // Assuming findByUsername method exists - if (!user.isPresent()) { - // Handle error appropriately - return "redirect:/error"; // Example redirection in case of error - } - // Add attributes to the model - model.addAttribute("username", username); - } - } else { - return "redirect:/"; - } - return "change-creds"; - } - - + // Retrieve username and other attributes + String username = userDetails.getUsername(); + + // Fetch user details from the database + Optional user = + userRepository.findByUsername( + username); // Assuming findByUsername method exists + if (!user.isPresent()) { + // Handle error appropriately + return "redirect:/error"; // Example redirection in case of error + } + // Add attributes to the model + model.addAttribute("username", username); + } + } else { + return "redirect:/"; + } + return "change-creds"; + } } diff --git a/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java b/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java index 3bad71c9..b34bac3c 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java @@ -25,14 +25,14 @@ public class ConverterWebController { model.addAttribute("currentPage", "html-to-pdf"); return "convert/html-to-pdf"; } + @GetMapping("/markdown-to-pdf") @Hidden public String convertMarkdownToPdfForm(Model model) { model.addAttribute("currentPage", "markdown-to-pdf"); return "convert/markdown-to-pdf"; } - - + @GetMapping("/url-to-pdf") @Hidden public String convertURLToPdfForm(Model model) { @@ -40,25 +40,22 @@ public class ConverterWebController { return "convert/url-to-pdf"; } - @GetMapping("/pdf-to-img") @Hidden public String pdfToimgForm(Model model) { model.addAttribute("currentPage", "pdf-to-img"); return "convert/pdf-to-img"; } - + @GetMapping("/file-to-pdf") @Hidden public String convertToPdfForm(Model model) { model.addAttribute("currentPage", "file-to-pdf"); return "convert/file-to-pdf"; } - - - //PDF TO...... - + // PDF TO...... + @GetMapping("/pdf-to-html") @Hidden public ModelAndView pdfToHTML() { @@ -107,7 +104,6 @@ public class ConverterWebController { return modelAndView; } - @GetMapping("/pdf-to-pdfa") @Hidden public String pdfToPdfAForm(Model model) { 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 b670a129..7b6489d6 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java @@ -32,70 +32,63 @@ 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"); + @GetMapping("/pipeline") + @Hidden + public String pipelineForm(Model model) { + model.addAttribute("currentPage", "pipeline"); - List pipelineConfigs = new ArrayList<>(); - List> pipelineConfigsWithNames = new ArrayList<>(); - - if(new File("./pipeline/defaultWebUIConfigs/").exists()) { - 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); - } - - for (String config : pipelineConfigs) { - Map jsonContent = new ObjectMapper().readValue(config, new TypeReference>(){}); - - String name = (String) jsonContent.get("name"); - Map configWithName = new HashMap<>(); - configWithName.put("json", config); - configWithName.put("name", name); - pipelineConfigsWithNames.add(configWithName); - } - - - - - } catch (IOException e) { - e.printStackTrace(); - } - } - if(pipelineConfigsWithNames.size() == 0) { - Map configWithName = new HashMap<>(); - configWithName.put("json", ""); - configWithName.put("name", "No preloaded configs found"); - pipelineConfigsWithNames.add(configWithName); - } - model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames); + List pipelineConfigs = new ArrayList<>(); + List> pipelineConfigsWithNames = new ArrayList<>(); - model.addAttribute("pipelineConfigs", pipelineConfigs); + if (new File("./pipeline/defaultWebUIConfigs/").exists()) { + try (Stream paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) { + List jsonFiles = + paths.filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith(".json")) + .collect(Collectors.toList()); - return "pipeline"; - } + for (Path jsonFile : jsonFiles) { + String content = Files.readString(jsonFile, StandardCharsets.UTF_8); + pipelineConfigs.add(content); + } + + for (String config : pipelineConfigs) { + Map jsonContent = + new ObjectMapper() + .readValue(config, new TypeReference>() {}); + + String name = (String) jsonContent.get("name"); + Map configWithName = new HashMap<>(); + configWithName.put("json", config); + configWithName.put("name", name); + pipelineConfigsWithNames.add(configWithName); + } + + } catch (IOException e) { + e.printStackTrace(); + } + } + if (pipelineConfigsWithNames.size() == 0) { + Map configWithName = new HashMap<>(); + configWithName.put("json", ""); + configWithName.put("name", "No preloaded configs found"); + pipelineConfigsWithNames.add(configWithName); + } + model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames); + + model.addAttribute("pipelineConfigs", pipelineConfigs); + + return "pipeline"; + } - - - @GetMapping("/merge-pdfs") @Hidden public String mergePdfForm(Model model) { model.addAttribute("currentPage", "merge-pdfs"); return "merge-pdfs"; } - + @GetMapping("/split-pdf-by-sections") @Hidden public String splitPdfBySections(Model model) { @@ -109,57 +102,56 @@ public class GeneralWebController { model.addAttribute("currentPage", "view-pdf"); return "view-pdf"; } - + @GetMapping("/multi-tool") @Hidden public String multiToolForm(Model model) { model.addAttribute("currentPage", "multi-tool"); return "multi-tool"; } - - + @GetMapping("/remove-pages") @Hidden public String pageDeleter(Model model) { model.addAttribute("currentPage", "remove-pages"); return "remove-pages"; } - + @GetMapping("/pdf-organizer") @Hidden public String pageOrganizer(Model model) { model.addAttribute("currentPage", "pdf-organizer"); return "pdf-organizer"; } - + @GetMapping("/extract-page") @Hidden public String extractPages(Model model) { model.addAttribute("currentPage", "extract-page"); return "extract-page"; } - + @GetMapping("/pdf-to-single-page") @Hidden public String pdfToSinglePage(Model model) { model.addAttribute("currentPage", "pdf-to-single-page"); return "pdf-to-single-page"; } - + @GetMapping("/rotate-pdf") @Hidden public String rotatePdfForm(Model model) { model.addAttribute("currentPage", "rotate-pdf"); return "rotate-pdf"; } - + @GetMapping("/split-pdfs") @Hidden public String splitPdfForm(Model model) { model.addAttribute("currentPage", "split-pdfs"); return "split-pdfs"; } - + @GetMapping("/sign") @Hidden public String signForm(Model model) { @@ -167,22 +159,20 @@ public class GeneralWebController { model.addAttribute("fonts", getFontNames()); return "sign"; } - + @GetMapping("/multi-page-layout") @Hidden public String multiPageLayoutForm(Model model) { model.addAttribute("currentPage", "multi-page-layout"); return "multi-page-layout"; } - - + @GetMapping("/scale-pages") @Hidden public String scalePagesFrom(Model model) { model.addAttribute("currentPage", "scale-pages"); return "scale-pages"; } - @GetMapping("/split-by-size-or-count") @Hidden @@ -190,18 +180,16 @@ public class GeneralWebController { model.addAttribute("currentPage", "split-by-size-or-count"); return "split-by-size-or-count"; } - + @GetMapping("/overlay-pdf") @Hidden public String overlayPdf(Model model) { model.addAttribute("currentPage", "overlay-pdf"); return "overlay-pdf"; } - - - @Autowired - private ResourceLoader resourceLoader; - + + @Autowired private ResourceLoader resourceLoader; + private List getFontNames() { List fontNames = new ArrayList<>(); @@ -216,25 +204,27 @@ public class GeneralWebController { private List getFontNamesFromLocation(String locationPattern) { try { - Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(resourceLoader) - .getResources(locationPattern); + Resource[] resources = + ResourcePatternUtils.getResourcePatternResolver(resourceLoader) + .getResources(locationPattern); return Arrays.stream(resources) - .map(resource -> { - try { - String filename = resource.getFilename(); - if (filename != null) { - int lastDotIndex = filename.lastIndexOf('.'); - if (lastDotIndex != -1) { - String name = filename.substring(0, lastDotIndex); - String extension = filename.substring(lastDotIndex + 1); - return new FontResource(name, extension); + .map( + resource -> { + try { + String filename = resource.getFilename(); + if (filename != null) { + int lastDotIndex = filename.lastIndexOf('.'); + if (lastDotIndex != -1) { + String name = filename.substring(0, lastDotIndex); + String extension = filename.substring(lastDotIndex + 1); + return new FontResource(name, extension); + } + } + return null; + } catch (Exception e) { + throw new RuntimeException("Error processing filename", e); } - } - return null; - } catch (Exception e) { - throw new RuntimeException("Error processing filename", e); - } - }) + }) .filter(Objects::nonNull) .collect(Collectors.toList()); } catch (Exception e) { @@ -242,64 +232,65 @@ public class GeneralWebController { } } - public String getFormatFromExtension(String extension) { switch (extension) { - case "ttf": return "truetype"; - case "woff": return "woff"; - case "woff2": return "woff2"; - case "eot": return "embedded-opentype"; - case "svg": return "svg"; - default: return ""; // or throw an exception if an unexpected extension is encountered + case "ttf": + return "truetype"; + case "woff": + return "woff"; + case "woff2": + return "woff2"; + case "eot": + return "embedded-opentype"; + case "svg": + return "svg"; + default: + return ""; // or throw an exception if an unexpected extension is encountered } } - public class FontResource { private String name; private String extension; private String type; + public FontResource(String name, String extension) { this.name = name; this.extension = extension; this.type = getFormatFromExtension(extension); } - public String getName() { - return name; - } + public String getName() { + return name; + } - public void setName(String name) { - this.name = name; - } + public void setName(String name) { + this.name = name; + } - public String getExtension() { - return extension; - } + public String getExtension() { + return extension; + } - public void setExtension(String extension) { - this.extension = extension; - } + public void setExtension(String extension) { + this.extension = extension; + } - public String getType() { - return type; - } + public String getType() { + return type; + } - public void setType(String type) { - this.type = type; - } - - + public void setType(String type) { + this.type = type; + } } - @GetMapping("/crop") @Hidden public String cropForm(Model model) { model.addAttribute("currentPage", "crop"); return "crop"; } - @GetMapping("/auto-split-pdf") @Hidden diff --git a/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java b/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java index b6d08fbf..23c19f9a 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java @@ -8,20 +8,19 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; import io.swagger.v3.oas.annotations.Hidden; + import stirling.software.SPDF.model.ApplicationProperties; @Controller public class HomeWebController { - + @GetMapping("/about") @Hidden public String gameForm(Model model) { model.addAttribute("currentPage", "about"); return "about"; } - - - + @GetMapping("/") public String home(Model model) { model.addAttribute("currentPage", "home"); @@ -32,21 +31,18 @@ public class HomeWebController { public String root(Model model) { return "redirect:/"; } - - @Autowired - ApplicationProperties applicationProperties; + @Autowired ApplicationProperties applicationProperties; @GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE) @ResponseBody @Hidden public String getRobotsTxt() { Boolean allowGoogle = applicationProperties.getSystem().getGooglevisibility(); - if(Boolean.TRUE.equals(allowGoogle)) { + if (Boolean.TRUE.equals(allowGoogle)) { return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /"; } else { return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /"; } } - } diff --git a/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java b/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java index dbb254a7..5b079042 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.controller.web; + import java.time.Duration; import java.time.LocalDateTime; import java.util.Comparator; @@ -22,6 +23,7 @@ import io.micrometer.core.instrument.MeterRegistry; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; + import jakarta.annotation.PostConstruct; import stirling.software.SPDF.config.StartupApplicationListener; import stirling.software.SPDF.model.ApplicationProperties; @@ -31,30 +33,28 @@ import stirling.software.SPDF.model.ApplicationProperties; @Tag(name = "Info", description = "Info APIs") public class MetricsController { - - @Autowired - ApplicationProperties applicationProperties; - - + @Autowired ApplicationProperties applicationProperties; + private final MeterRegistry meterRegistry; private boolean metricsEnabled; - + @PostConstruct public void init() { - Boolean metricsEnabled = applicationProperties.getMetrics().getEnabled(); - if(metricsEnabled == null) - metricsEnabled = true; + Boolean metricsEnabled = applicationProperties.getMetrics().getEnabled(); + if (metricsEnabled == null) metricsEnabled = true; this.metricsEnabled = metricsEnabled; } - + public MetricsController(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; } @GetMapping("/status") - @Operation(summary = "Application status and version", - description = "This endpoint returns the status of the application and its version number.") + @Operation( + summary = "Application status and version", + description = + "This endpoint returns the status of the application and its version number.") public ResponseEntity getStatus() { if (!metricsEnabled) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); @@ -65,38 +65,46 @@ public class MetricsController { status.put("version", getClass().getPackage().getImplementationVersion()); return ResponseEntity.ok(status); } - + @GetMapping("/loads") - @Operation(summary = "GET request count", - description = "This endpoint returns the total count of GET requests or the count of GET requests for a specific endpoint.") - public ResponseEntity getPageLoads(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional endpoint) { - if (!metricsEnabled) { + @Operation( + summary = "GET request count", + description = + "This endpoint returns the total count of GET requests or the count of GET requests for a specific endpoint.") + public ResponseEntity getPageLoads( + @RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") + Optional endpoint) { + if (!metricsEnabled) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); } - try { + try { double count = 0.0; - + for (Meter meter : meterRegistry.getMeters()) { if (meter.getId().getName().equals("http.requests")) { String method = meter.getId().getTag("method"); if (method != null && method.equals("GET")) { - - if (endpoint.isPresent() && !endpoint.get().isBlank()) { - if(!endpoint.get().startsWith("/")) { - endpoint = Optional.of("/" + endpoint.get()); - } - System.out.println("loads " + endpoint.get() + " vs " + meter.getId().getTag("uri")); - if(endpoint.get().equals(meter.getId().getTag("uri"))){ - if (meter instanceof Counter) { - count += ((Counter) meter).count(); - } - } - } else { - if (meter instanceof Counter) { - count += ((Counter) meter).count(); - } - } + + if (endpoint.isPresent() && !endpoint.get().isBlank()) { + if (!endpoint.get().startsWith("/")) { + endpoint = Optional.of("/" + endpoint.get()); + } + System.out.println( + "loads " + + endpoint.get() + + " vs " + + meter.getId().getTag("uri")); + if (endpoint.get().equals(meter.getId().getTag("uri"))) { + if (meter instanceof Counter) { + count += ((Counter) meter).count(); + } + } + } else { + if (meter instanceof Counter) { + count += ((Counter) meter).count(); + } + } } } } @@ -108,10 +116,11 @@ public class MetricsController { } @GetMapping("/loads/all") - @Operation(summary = "GET requests count for all endpoints", + @Operation( + summary = "GET requests count for all endpoints", description = "This endpoint returns the count of GET requests for each endpoint.") public ResponseEntity getAllEndpointLoads() { - if (!metricsEnabled) { + if (!metricsEnabled) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); } try { @@ -133,10 +142,11 @@ public class MetricsController { } } - List results = counts.entrySet().stream() - .map(entry -> new EndpointCount(entry.getKey(), entry.getValue())) - .sorted(Comparator.comparing(EndpointCount::getCount).reversed()) - .collect(Collectors.toList()); + List results = + counts.entrySet().stream() + .map(entry -> new EndpointCount(entry.getKey(), entry.getValue())) + .sorted(Comparator.comparing(EndpointCount::getCount).reversed()) + .collect(Collectors.toList()); return ResponseEntity.ok(results); } catch (Exception e) { @@ -147,35 +157,41 @@ public class MetricsController { public class EndpointCount { private String endpoint; private double count; - - public EndpointCount(String endpoint, double count) { - this.endpoint = endpoint; - this.count = count; - } - public String getEndpoint() { - return endpoint; - } - public void setEndpoint(String endpoint) { - this.endpoint = endpoint; - } - public double getCount() { - return count; - } - public void setCount(double count) { - this.count = count; - } + public EndpointCount(String endpoint, double count) { + this.endpoint = endpoint; + this.count = count; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public double getCount() { + return count; + } + + public void setCount(double count) { + this.count = count; + } } - @GetMapping("/requests") - @Operation(summary = "POST request count", - description = "This endpoint returns the total count of POST requests or the count of POST requests for a specific endpoint.") - public ResponseEntity getTotalRequests(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional endpoint) { - if (!metricsEnabled) { + @Operation( + summary = "POST request count", + description = + "This endpoint returns the total count of POST requests or the count of POST requests for a specific endpoint.") + public ResponseEntity getTotalRequests( + @RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") + Optional endpoint) { + if (!metricsEnabled) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); } - try { + try { double count = 0.0; for (Meter meter : meterRegistry.getMeters()) { @@ -199,18 +215,18 @@ public class MetricsController { } } } - return ResponseEntity.ok(count); + return ResponseEntity.ok(count); } catch (Exception e) { - return ResponseEntity.ok(-1); + return ResponseEntity.ok(-1); } } - @GetMapping("/requests/all") - @Operation(summary = "POST requests count for all endpoints", + @Operation( + summary = "POST requests count for all endpoints", description = "This endpoint returns the count of POST requests for each endpoint.") public ResponseEntity getAllPostRequests() { - if (!metricsEnabled) { + if (!metricsEnabled) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); } try { @@ -232,10 +248,11 @@ public class MetricsController { } } - List results = counts.entrySet().stream() - .map(entry -> new EndpointCount(entry.getKey(), entry.getValue())) - .sorted(Comparator.comparing(EndpointCount::getCount).reversed()) - .collect(Collectors.toList()); + List results = + counts.entrySet().stream() + .map(entry -> new EndpointCount(entry.getKey(), entry.getValue())) + .sorted(Comparator.comparing(EndpointCount::getCount).reversed()) + .collect(Collectors.toList()); return ResponseEntity.ok(results); } catch (Exception e) { @@ -243,7 +260,6 @@ public class MetricsController { } } - @GetMapping("/uptime") public ResponseEntity getUptime() { if (!metricsEnabled) { diff --git a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java index ce4ae649..b0204779 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java @@ -23,7 +23,7 @@ public class OtherWebController { model.addAttribute("currentPage", "compress-pdf"); return "misc/compress-pdf"; } - + @GetMapping("/extract-image-scans") @Hidden public ModelAndView extractImageScansForm() { @@ -31,37 +31,34 @@ public class OtherWebController { modelAndView.addObject("currentPage", "extract-image-scans"); return modelAndView; } - + @GetMapping("/show-javascript") @Hidden public String extractJavascriptForm(Model model) { model.addAttribute("currentPage", "show-javascript"); return "misc/show-javascript"; } - - + @GetMapping("/add-page-numbers") @Hidden public String addPageNumbersForm(Model model) { model.addAttribute("currentPage", "add-page-numbers"); return "misc/add-page-numbers"; } - + @GetMapping("/extract-images") @Hidden public String extractImagesForm(Model model) { model.addAttribute("currentPage", "extract-images"); return "misc/extract-images"; } - + @GetMapping("/flatten") @Hidden public String flattenForm(Model model) { model.addAttribute("currentPage", "flatten"); return "misc/flatten"; } - - @GetMapping("/change-metadata") @Hidden @@ -69,22 +66,25 @@ public class OtherWebController { model.addAttribute("currentPage", "change-metadata"); return "misc/change-metadata"; } - + @GetMapping("/compare") @Hidden public String compareForm(Model model) { model.addAttribute("currentPage", "compare"); return "misc/compare"; } - + public List getAvailableTesseractLanguages() { String tessdataDir = "/usr/share/tesseract-ocr/5/tessdata"; File[] files = new File(tessdataDir).listFiles(); if (files == null) { return Collections.emptyList(); } - return Arrays.stream(files).filter(file -> file.getName().endsWith(".traineddata")).map(file -> file.getName().replace(".traineddata", "")) - .filter(lang -> !lang.equalsIgnoreCase("osd")).collect(Collectors.toList()); + return Arrays.stream(files) + .filter(file -> file.getName().endsWith(".traineddata")) + .map(file -> file.getName().replace(".traineddata", "")) + .filter(lang -> !lang.equalsIgnoreCase("osd")) + .collect(Collectors.toList()); } @GetMapping("/ocr-pdf") @@ -97,29 +97,28 @@ public class OtherWebController { modelAndView.addObject("currentPage", "ocr-pdf"); return modelAndView; } - - + @GetMapping("/add-image") @Hidden public String overlayImage(Model model) { model.addAttribute("currentPage", "add-image"); return "misc/add-image"; } - + @GetMapping("/adjust-contrast") @Hidden public String contrast(Model model) { model.addAttribute("currentPage", "adjust-contrast"); return "misc/adjust-contrast"; } - + @GetMapping("/repair") @Hidden public String repairForm(Model model) { model.addAttribute("currentPage", "repair"); return "misc/repair"; } - + @GetMapping("/remove-blanks") @Hidden public String removeBlanksForm(Model model) { @@ -140,14 +139,11 @@ public class OtherWebController { model.addAttribute("currentPage", "auto-crop"); return "misc/auto-crop"; } - + @GetMapping("/auto-rename") @Hidden public String autoRenameForm(Model model) { model.addAttribute("currentPage", "auto-rename"); return "misc/auto-rename"; } - - - } diff --git a/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java b/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java index 2cbf245f..ba67e1d7 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java @@ -10,20 +10,21 @@ import io.swagger.v3.oas.annotations.tags.Tag; @Controller @Tag(name = "Security", description = "Security APIs") public class SecurityWebController { - - @GetMapping("/auto-redact") + + @GetMapping("/auto-redact") @Hidden public String autoRedactForm(Model model) { model.addAttribute("currentPage", "auto-redact"); return "security/auto-redact"; } - + @GetMapping("/add-password") @Hidden public String addPasswordForm(Model model) { model.addAttribute("currentPage", "add-password"); return "security/add-password"; } + @GetMapping("/change-permissions") @Hidden public String permissionsForm(Model model) { @@ -44,21 +45,21 @@ public class SecurityWebController { model.addAttribute("currentPage", "add-watermark"); return "security/add-watermark"; } - + @GetMapping("/cert-sign") @Hidden public String certSignForm(Model model) { model.addAttribute("currentPage", "cert-sign"); return "security/cert-sign"; } - + @GetMapping("/sanitize-pdf") @Hidden public String sanitizeForm(Model model) { model.addAttribute("currentPage", "sanitize-pdf"); return "security/sanitize-pdf"; } - + @GetMapping("/get-info-on-pdf") @Hidden public String getInfo(Model model) { diff --git a/src/main/java/stirling/software/SPDF/model/ApiEndpoint.java b/src/main/java/stirling/software/SPDF/model/ApiEndpoint.java index 1838b763..0d707e3b 100644 --- a/src/main/java/stirling/software/SPDF/model/ApiEndpoint.java +++ b/src/main/java/stirling/software/SPDF/model/ApiEndpoint.java @@ -9,14 +9,16 @@ public class ApiEndpoint { private String name; private Map parameters; private String description; - + public ApiEndpoint(String name, JsonNode postNode) { this.name = name; this.parameters = new HashMap<>(); - postNode.path("parameters").forEach(paramNode -> { - String paramName = paramNode.path("name").asText(); - parameters.put(paramName, paramNode); - }); + postNode.path("parameters") + .forEach( + paramNode -> { + String paramName = paramNode.path("name").asText(); + parameters.put(paramName, paramNode); + }); this.description = postNode.path("description").asText(); } @@ -32,11 +34,9 @@ public class ApiEndpoint { public String getDescription() { return description; } - - @Override - public String toString() { - return "ApiEndpoint [name=" + name + ", parameters=" + parameters + "]"; - } - - -} \ No newline at end of file + + @Override + public String toString() { + return "ApiEndpoint [name=" + name + ", parameters=" + parameters + "]"; + } +} diff --git a/src/main/java/stirling/software/SPDF/model/ApiKeyAuthenticationToken.java b/src/main/java/stirling/software/SPDF/model/ApiKeyAuthenticationToken.java index 54b61c1b..f19fa1e9 100644 --- a/src/main/java/stirling/software/SPDF/model/ApiKeyAuthenticationToken.java +++ b/src/main/java/stirling/software/SPDF/model/ApiKeyAuthenticationToken.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.model; + import java.util.Collection; import org.springframework.security.authentication.AbstractAuthenticationToken; @@ -16,9 +17,10 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { setAuthenticated(false); } - public ApiKeyAuthenticationToken(Object principal, String apiKey, Collection authorities) { + public ApiKeyAuthenticationToken( + Object principal, String apiKey, Collection authorities) { super(authorities); - this.principal = principal; // principal can be a UserDetails object + this.principal = principal; // principal can be a UserDetails object this.credentials = apiKey; super.setAuthenticated(true); // this authentication is trusted } @@ -36,7 +38,8 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { if (isAuthenticated) { - throw new IllegalArgumentException("Cannot set this token to trusted. Use constructor which takes a GrantedAuthority list instead."); + throw new IllegalArgumentException( + "Cannot set this token to trusted. Use constructor which takes a GrantedAuthority list instead."); } super.setAuthenticated(false); } diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java index 36073c88..a41d641c 100644 --- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java +++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java @@ -12,357 +12,376 @@ import stirling.software.SPDF.config.YamlPropertySourceFactory; @ConfigurationProperties(prefix = "") @PropertySource(value = "file:./configs/settings.yml", factory = YamlPropertySourceFactory.class) public class ApplicationProperties { - private Security security; - private System system; - private Ui ui; - private Endpoints endpoints; - private Metrics metrics; - private AutomaticallyGenerated automaticallyGenerated; - private AutoPipeline autoPipeline; - - public AutoPipeline getAutoPipeline() { - return autoPipeline != null ? autoPipeline : new AutoPipeline(); - } - - public void setAutoPipeline(AutoPipeline autoPipeline) { - this.autoPipeline = autoPipeline; - } - - public Security getSecurity() { - return security != null ? security : new Security(); - } - - public void setSecurity(Security security) { - this.security = security; - } - - public System getSystem() { - return system != null ? system : new System(); - } - - public void setSystem(System system) { - this.system = system; - } - - public Ui getUi() { - return ui != null ? ui : new Ui(); - } - - public void setUi(Ui ui) { - this.ui = ui; - } - - public Endpoints getEndpoints() { - return endpoints != null ? endpoints : new Endpoints(); - } - - public void setEndpoints(Endpoints endpoints) { - this.endpoints = endpoints; - } - - public Metrics getMetrics() { - return metrics != null ? metrics : new Metrics(); - } - - public void setMetrics(Metrics metrics) { - this.metrics = metrics; - } - - public AutomaticallyGenerated getAutomaticallyGenerated() { - return automaticallyGenerated != null ? automaticallyGenerated : new AutomaticallyGenerated(); - } - - public void setAutomaticallyGenerated(AutomaticallyGenerated automaticallyGenerated) { - this.automaticallyGenerated = automaticallyGenerated; - } - - @Override - public String toString() { - return "ApplicationProperties [security=" + security + ", system=" + system + ", ui=" + ui + ", endpoints=" - + endpoints + ", metrics=" + metrics + ", automaticallyGenerated=" + automaticallyGenerated - + ", autoPipeline=" + autoPipeline + "]"; - } - - public static class AutoPipeline { - private String outputFolder; - - public String getOutputFolder() { - return outputFolder; - } - - public void setOutputFolder(String outputFolder) { - this.outputFolder = outputFolder; - } - - @Override - public String toString() { - return "AutoPipeline [outputFolder=" + outputFolder + "]"; - } - - - - } - public static class Security { - private Boolean enableLogin; - private Boolean csrfDisabled; - private InitialLogin initialLogin; - private int loginAttemptCount; - private long loginResetTimeMinutes; - - - public int getLoginAttemptCount() { - return loginAttemptCount; - } - - public void setLoginAttemptCount(int loginAttemptCount) { - this.loginAttemptCount = loginAttemptCount; - } - - public long getLoginResetTimeMinutes() { - return loginResetTimeMinutes; - } - - public void setLoginResetTimeMinutes(long loginResetTimeMinutes) { - this.loginResetTimeMinutes = loginResetTimeMinutes; - } - - public InitialLogin getInitialLogin() { - return initialLogin != null ? initialLogin : new InitialLogin(); - } - - public void setInitialLogin(InitialLogin initialLogin) { - this.initialLogin = initialLogin; - } - - public Boolean getEnableLogin() { - return enableLogin; - } - - public void setEnableLogin(Boolean enableLogin) { - this.enableLogin = enableLogin; - } - - public Boolean getCsrfDisabled() { - return csrfDisabled; - } - - public void setCsrfDisabled(Boolean csrfDisabled) { - this.csrfDisabled = csrfDisabled; - } - - - @Override - public String toString() { - return "Security [enableLogin=" + enableLogin + ", initialLogin=" + initialLogin + ", csrfDisabled=" - + csrfDisabled + "]"; - } - - public static class InitialLogin { - - private String username; - private String password; - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - @Override - public String toString() { - return "InitialLogin [username=" + username + ", password=" + (password != null && !password.isEmpty() ? "MASKED" : "NULL") + "]"; - } - - - - } - } - - public static class System { - private String defaultLocale; - private Boolean googlevisibility; - private String rootURIPath; - private String customStaticFilePath; - private Integer maxFileSize; - - private Boolean enableAlphaFunctionality; - - - - - public Boolean getEnableAlphaFunctionality() { - return enableAlphaFunctionality; - } - - public void setEnableAlphaFunctionality(Boolean enableAlphaFunctionality) { - this.enableAlphaFunctionality = enableAlphaFunctionality; - } - - public String getDefaultLocale() { - return defaultLocale; - } - - public void setDefaultLocale(String defaultLocale) { - this.defaultLocale = defaultLocale; - } - - public Boolean getGooglevisibility() { - return googlevisibility; - } - - public void setGooglevisibility(Boolean googlevisibility) { - this.googlevisibility = googlevisibility; - } - - public String getRootURIPath() { - return rootURIPath; - } - - public void setRootURIPath(String rootURIPath) { - this.rootURIPath = rootURIPath; - } - - public String getCustomStaticFilePath() { - return customStaticFilePath; - } - - public void setCustomStaticFilePath(String customStaticFilePath) { - this.customStaticFilePath = customStaticFilePath; - } - - public Integer getMaxFileSize() { - return maxFileSize; - } - - public void setMaxFileSize(Integer maxFileSize) { - this.maxFileSize = maxFileSize; - } - - @Override - public String toString() { - return "System [defaultLocale=" + defaultLocale + ", googlevisibility=" + googlevisibility - + ", rootURIPath=" + rootURIPath + ", customStaticFilePath=" + customStaticFilePath - + ", maxFileSize=" + maxFileSize + ", enableAlphaFunctionality=" + enableAlphaFunctionality + "]"; - } - - - - } - - public static class Ui { - private String appName; - private String homeDescription; - private String appNameNavbar; - - public String getAppName() { - if(appName != null && appName.trim().length() == 0) - return null; - return appName; - } - - public void setAppName(String appName) { - this.appName = appName; - } - - public String getHomeDescription() { - if(homeDescription != null && homeDescription.trim().length() == 0) - return null; - return homeDescription; - } - - public void setHomeDescription(String homeDescription) { - this.homeDescription = homeDescription; - } - - public String getAppNameNavbar() { - if(appNameNavbar != null && appNameNavbar.trim().length() == 0) - return null; - return appNameNavbar; - } - - public void setAppNameNavbar(String appNameNavbar) { - this.appNameNavbar = appNameNavbar; - } - - @Override - public String toString() { - return "UserInterface [appName=" + appName + ", homeDescription=" + homeDescription + ", appNameNavbar=" + appNameNavbar + "]"; - } - } - - - public static class Endpoints { - private List toRemove; - private List groupsToRemove; - - public List getToRemove() { - return toRemove; - } - - public void setToRemove(List toRemove) { - this.toRemove = toRemove; - } - - public List getGroupsToRemove() { - return groupsToRemove; - } - - public void setGroupsToRemove(List groupsToRemove) { - this.groupsToRemove = groupsToRemove; - } - - @Override - public String toString() { - return "Endpoints [toRemove=" + toRemove + ", groupsToRemove=" + groupsToRemove + "]"; - } - - - } - - public static class Metrics { - private Boolean enabled; - - public Boolean getEnabled() { - return enabled; - } - - public void setEnabled(Boolean enabled) { - this.enabled = enabled; - } - - @Override - public String toString() { - return "Metrics [enabled=" + enabled + "]"; - } - - - } - - public static class AutomaticallyGenerated { - private String key; - - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } - - @Override - public String toString() { - return "AutomaticallyGenerated [key=" + (key != null && !key.isEmpty() ? "MASKED" : "NULL") + "]"; - } - - } + private Security security; + private System system; + private Ui ui; + private Endpoints endpoints; + private Metrics metrics; + private AutomaticallyGenerated automaticallyGenerated; + private AutoPipeline autoPipeline; + + public AutoPipeline getAutoPipeline() { + return autoPipeline != null ? autoPipeline : new AutoPipeline(); + } + + public void setAutoPipeline(AutoPipeline autoPipeline) { + this.autoPipeline = autoPipeline; + } + + public Security getSecurity() { + return security != null ? security : new Security(); + } + + public void setSecurity(Security security) { + this.security = security; + } + + public System getSystem() { + return system != null ? system : new System(); + } + + public void setSystem(System system) { + this.system = system; + } + + public Ui getUi() { + return ui != null ? ui : new Ui(); + } + + public void setUi(Ui ui) { + this.ui = ui; + } + + public Endpoints getEndpoints() { + return endpoints != null ? endpoints : new Endpoints(); + } + + public void setEndpoints(Endpoints endpoints) { + this.endpoints = endpoints; + } + + public Metrics getMetrics() { + return metrics != null ? metrics : new Metrics(); + } + + public void setMetrics(Metrics metrics) { + this.metrics = metrics; + } + + public AutomaticallyGenerated getAutomaticallyGenerated() { + return automaticallyGenerated != null + ? automaticallyGenerated + : new AutomaticallyGenerated(); + } + + public void setAutomaticallyGenerated(AutomaticallyGenerated automaticallyGenerated) { + this.automaticallyGenerated = automaticallyGenerated; + } + + @Override + public String toString() { + return "ApplicationProperties [security=" + + security + + ", system=" + + system + + ", ui=" + + ui + + ", endpoints=" + + endpoints + + ", metrics=" + + metrics + + ", automaticallyGenerated=" + + automaticallyGenerated + + ", autoPipeline=" + + autoPipeline + + "]"; + } + + public static class AutoPipeline { + private String outputFolder; + + public String getOutputFolder() { + return outputFolder; + } + + public void setOutputFolder(String outputFolder) { + this.outputFolder = outputFolder; + } + + @Override + public String toString() { + return "AutoPipeline [outputFolder=" + outputFolder + "]"; + } + } + + public static class Security { + private Boolean enableLogin; + private Boolean csrfDisabled; + private InitialLogin initialLogin; + private int loginAttemptCount; + private long loginResetTimeMinutes; + + public int getLoginAttemptCount() { + return loginAttemptCount; + } + + public void setLoginAttemptCount(int loginAttemptCount) { + this.loginAttemptCount = loginAttemptCount; + } + + public long getLoginResetTimeMinutes() { + return loginResetTimeMinutes; + } + + public void setLoginResetTimeMinutes(long loginResetTimeMinutes) { + this.loginResetTimeMinutes = loginResetTimeMinutes; + } + + public InitialLogin getInitialLogin() { + return initialLogin != null ? initialLogin : new InitialLogin(); + } + + public void setInitialLogin(InitialLogin initialLogin) { + this.initialLogin = initialLogin; + } + + public Boolean getEnableLogin() { + return enableLogin; + } + + public void setEnableLogin(Boolean enableLogin) { + this.enableLogin = enableLogin; + } + + public Boolean getCsrfDisabled() { + return csrfDisabled; + } + + public void setCsrfDisabled(Boolean csrfDisabled) { + this.csrfDisabled = csrfDisabled; + } + + @Override + public String toString() { + return "Security [enableLogin=" + + enableLogin + + ", initialLogin=" + + initialLogin + + ", csrfDisabled=" + + csrfDisabled + + "]"; + } + + public static class InitialLogin { + + private String username; + private String password; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public String toString() { + return "InitialLogin [username=" + + username + + ", password=" + + (password != null && !password.isEmpty() ? "MASKED" : "NULL") + + "]"; + } + } + } + + public static class System { + private String defaultLocale; + private Boolean googlevisibility; + private String rootURIPath; + private String customStaticFilePath; + private Integer maxFileSize; + + private Boolean enableAlphaFunctionality; + + public Boolean getEnableAlphaFunctionality() { + return enableAlphaFunctionality; + } + + public void setEnableAlphaFunctionality(Boolean enableAlphaFunctionality) { + this.enableAlphaFunctionality = enableAlphaFunctionality; + } + + public String getDefaultLocale() { + return defaultLocale; + } + + public void setDefaultLocale(String defaultLocale) { + this.defaultLocale = defaultLocale; + } + + public Boolean getGooglevisibility() { + return googlevisibility; + } + + public void setGooglevisibility(Boolean googlevisibility) { + this.googlevisibility = googlevisibility; + } + + public String getRootURIPath() { + return rootURIPath; + } + + public void setRootURIPath(String rootURIPath) { + this.rootURIPath = rootURIPath; + } + + public String getCustomStaticFilePath() { + return customStaticFilePath; + } + + public void setCustomStaticFilePath(String customStaticFilePath) { + this.customStaticFilePath = customStaticFilePath; + } + + public Integer getMaxFileSize() { + return maxFileSize; + } + + public void setMaxFileSize(Integer maxFileSize) { + this.maxFileSize = maxFileSize; + } + + @Override + public String toString() { + return "System [defaultLocale=" + + defaultLocale + + ", googlevisibility=" + + googlevisibility + + ", rootURIPath=" + + rootURIPath + + ", customStaticFilePath=" + + customStaticFilePath + + ", maxFileSize=" + + maxFileSize + + ", enableAlphaFunctionality=" + + enableAlphaFunctionality + + "]"; + } + } + + public static class Ui { + private String appName; + private String homeDescription; + private String appNameNavbar; + + public String getAppName() { + if (appName != null && appName.trim().length() == 0) return null; + return appName; + } + + public void setAppName(String appName) { + this.appName = appName; + } + + public String getHomeDescription() { + if (homeDescription != null && homeDescription.trim().length() == 0) return null; + return homeDescription; + } + + public void setHomeDescription(String homeDescription) { + this.homeDescription = homeDescription; + } + + public String getAppNameNavbar() { + if (appNameNavbar != null && appNameNavbar.trim().length() == 0) return null; + return appNameNavbar; + } + + public void setAppNameNavbar(String appNameNavbar) { + this.appNameNavbar = appNameNavbar; + } + + @Override + public String toString() { + return "UserInterface [appName=" + + appName + + ", homeDescription=" + + homeDescription + + ", appNameNavbar=" + + appNameNavbar + + "]"; + } + } + + public static class Endpoints { + private List toRemove; + private List groupsToRemove; + + public List getToRemove() { + return toRemove; + } + + public void setToRemove(List toRemove) { + this.toRemove = toRemove; + } + + public List getGroupsToRemove() { + return groupsToRemove; + } + + public void setGroupsToRemove(List groupsToRemove) { + this.groupsToRemove = groupsToRemove; + } + + @Override + public String toString() { + return "Endpoints [toRemove=" + toRemove + ", groupsToRemove=" + groupsToRemove + "]"; + } + } + + public static class Metrics { + private Boolean enabled; + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + @Override + public String toString() { + return "Metrics [enabled=" + enabled + "]"; + } + } + + public static class AutomaticallyGenerated { + private String key; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @Override + public String toString() { + return "AutomaticallyGenerated [key=" + + (key != null && !key.isEmpty() ? "MASKED" : "NULL") + + "]"; + } + } } diff --git a/src/main/java/stirling/software/SPDF/model/AttemptCounter.java b/src/main/java/stirling/software/SPDF/model/AttemptCounter.java index fd668b05..7cb13ee0 100644 --- a/src/main/java/stirling/software/SPDF/model/AttemptCounter.java +++ b/src/main/java/stirling/software/SPDF/model/AttemptCounter.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.model; + public class AttemptCounter { private int attemptCount; private long lastAttemptTime; diff --git a/src/main/java/stirling/software/SPDF/model/Authority.java b/src/main/java/stirling/software/SPDF/model/Authority.java index 8be853ea..57ba538e 100644 --- a/src/main/java/stirling/software/SPDF/model/Authority.java +++ b/src/main/java/stirling/software/SPDF/model/Authority.java @@ -13,19 +13,15 @@ import jakarta.persistence.Table; @Table(name = "authorities") public class Authority { - public Authority() { + public Authority() {} - } - - - public Authority(String authority, User user) { - this.authority = authority; - this.user = user; - user.getAuthorities().add(this); - } + public Authority(String authority, User user) { + this.authority = authority; + this.user = user; + user.getAuthorities().add(this); + } - - @Id + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -36,29 +32,27 @@ public class Authority { @JoinColumn(name = "user_id") private User user; - public Long getId() { - return id; - } + public Long getId() { + return id; + } - public void setId(Long id) { - this.id = id; - } + public void setId(Long id) { + this.id = id; + } - public String getAuthority() { - return authority; - } + public String getAuthority() { + return authority; + } - public void setAuthority(String authority) { - this.authority = authority; - } + public void setAuthority(String authority) { + this.authority = authority; + } - public User getUser() { - return user; - } + public User getUser() { + return user; + } - public void setUser(User user) { - this.user = user; - } - - + public void setUser(User user) { + this.user = user; + } } diff --git a/src/main/java/stirling/software/SPDF/model/PDFText.java b/src/main/java/stirling/software/SPDF/model/PDFText.java index 9a4909d0..9c460f3c 100644 --- a/src/main/java/stirling/software/SPDF/model/PDFText.java +++ b/src/main/java/stirling/software/SPDF/model/PDFText.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.model; + public class PDFText { private final int pageIndex; private final float x1; @@ -39,4 +40,4 @@ public class PDFText { public String getText() { return text; } -} \ No newline at end of file +} diff --git a/src/main/java/stirling/software/SPDF/model/PersistentLogin.java b/src/main/java/stirling/software/SPDF/model/PersistentLogin.java index 0747c1eb..cc94eea2 100644 --- a/src/main/java/stirling/software/SPDF/model/PersistentLogin.java +++ b/src/main/java/stirling/software/SPDF/model/PersistentLogin.java @@ -24,38 +24,37 @@ public class PersistentLogin { @Column(name = "last_used", nullable = false) private Date lastUsed; - public String getSeries() { - return series; - } + public String getSeries() { + return series; + } - public void setSeries(String series) { - this.series = series; - } + public void setSeries(String series) { + this.series = series; + } - public String getUsername() { - return username; - } + public String getUsername() { + return username; + } - public void setUsername(String username) { - this.username = username; - } + public void setUsername(String username) { + this.username = username; + } - public String getToken() { - return token; - } + public String getToken() { + return token; + } - public void setToken(String token) { - this.token = token; - } + public void setToken(String token) { + this.token = token; + } - public Date getLastUsed() { - return lastUsed; - } + public Date getLastUsed() { + return lastUsed; + } - public void setLastUsed(Date lastUsed) { - this.lastUsed = lastUsed; - } + public void setLastUsed(Date lastUsed) { + this.lastUsed = lastUsed; + } - // Getters, setters, etc. } diff --git a/src/main/java/stirling/software/SPDF/model/PipelineConfig.java b/src/main/java/stirling/software/SPDF/model/PipelineConfig.java index 77ef7a05..efb9b232 100644 --- a/src/main/java/stirling/software/SPDF/model/PipelineConfig.java +++ b/src/main/java/stirling/software/SPDF/model/PipelineConfig.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.model; + import java.util.List; import com.fasterxml.jackson.annotation.JsonProperty; @@ -14,7 +15,6 @@ public class PipelineConfig { @JsonProperty("outputFileName") private String outputPattern; - public String getName() { return name; } @@ -46,6 +46,4 @@ public class PipelineConfig { public void setOutputPattern(String outputPattern) { this.outputPattern = outputPattern; } - - } diff --git a/src/main/java/stirling/software/SPDF/model/PipelineOperation.java b/src/main/java/stirling/software/SPDF/model/PipelineOperation.java index 10c27bfc..f6183505 100644 --- a/src/main/java/stirling/software/SPDF/model/PipelineOperation.java +++ b/src/main/java/stirling/software/SPDF/model/PipelineOperation.java @@ -3,30 +3,27 @@ package stirling.software.SPDF.model; import java.util.Map; public class PipelineOperation { - private String operation; - private Map parameters; + private String operation; + private Map parameters; + public String getOperation() { + return operation; + } - public String getOperation() { - return operation; - } + public void setOperation(String operation) { + this.operation = operation; + } - public void setOperation(String operation) { - this.operation = operation; - } + public Map getParameters() { + return parameters; + } - public Map getParameters() { - return parameters; - } + public void setParameters(Map parameters) { + this.parameters = parameters; + } - public void setParameters(Map parameters) { - this.parameters = parameters; - } - - @Override - public String toString() { - return "PipelineOperation [operation=" + operation + ", parameters=" + parameters + "]"; - } - - - } \ No newline at end of file + @Override + public String toString() { + return "PipelineOperation [operation=" + operation + ", parameters=" + parameters + "]"; + } +} diff --git a/src/main/java/stirling/software/SPDF/model/Role.java b/src/main/java/stirling/software/SPDF/model/Role.java index 85315a16..5100e9dd 100644 --- a/src/main/java/stirling/software/SPDF/model/Role.java +++ b/src/main/java/stirling/software/SPDF/model/Role.java @@ -1,7 +1,8 @@ package stirling.software.SPDF.model; + public enum Role { - - // Unlimited access + + // Unlimited access ADMIN("ROLE_ADMIN", Integer.MAX_VALUE, Integer.MAX_VALUE), // Unlimited access @@ -15,12 +16,11 @@ public enum Role { // 0 API calls per day and 20 web calls WEB_ONLY_USER("ROLE_WEB_ONLY_USER", 0, 20), - - - INTERNAL_API_USER("STIRLING-PDF-BACKEND-API-USER", Integer.MAX_VALUE, Integer.MAX_VALUE), - DEMO_USER("ROLE_DEMO_USER", 100, 100); - + INTERNAL_API_USER("STIRLING-PDF-BACKEND-API-USER", Integer.MAX_VALUE, Integer.MAX_VALUE), + + DEMO_USER("ROLE_DEMO_USER", 100, 100); + private final String roleId; private final int apiCallsPerDay; private final int webCallsPerDay; @@ -42,7 +42,7 @@ public enum Role { public int getWebCallsPerDay() { return webCallsPerDay; } - + public static Role fromString(String roleId) { for (Role role : Role.values()) { if (role.getRoleId().equalsIgnoreCase(roleId)) { @@ -51,5 +51,4 @@ public enum Role { } throw new IllegalArgumentException("No Role defined for id: " + roleId); } - } diff --git a/src/main/java/stirling/software/SPDF/model/SortTypes.java b/src/main/java/stirling/software/SPDF/model/SortTypes.java index 21181cfa..eb10ff58 100644 --- a/src/main/java/stirling/software/SPDF/model/SortTypes.java +++ b/src/main/java/stirling/software/SPDF/model/SortTypes.java @@ -1,4 +1,12 @@ package stirling.software.SPDF.model; + public enum SortTypes { - REVERSE_ORDER, DUPLEX_SORT, BOOKLET_SORT, SIDE_STITCH_BOOKLET_SORT, ODD_EVEN_SPLIT, REMOVE_FIRST, REMOVE_LAST, REMOVE_FIRST_AND_LAST, -} \ No newline at end of file + REVERSE_ORDER, + DUPLEX_SORT, + BOOKLET_SORT, + SIDE_STITCH_BOOKLET_SORT, + ODD_EVEN_SPLIT, + REMOVE_FIRST, + REMOVE_LAST, + REMOVE_FIRST_AND_LAST, +} diff --git a/src/main/java/stirling/software/SPDF/model/User.java b/src/main/java/stirling/software/SPDF/model/User.java index f771a821..253b33da 100644 --- a/src/main/java/stirling/software/SPDF/model/User.java +++ b/src/main/java/stirling/software/SPDF/model/User.java @@ -19,15 +19,16 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.MapKeyColumn; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; + @Entity @Table(name = "users") public class User { - @Id + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "user_id") private Long id; - + @Column(name = "username", unique = true) private String username; @@ -36,13 +37,13 @@ public class User { @Column(name = "apiKey") private String apiKey; - + @Column(name = "enabled") private boolean enabled; @Column(name = "isFirstLogin") private Boolean isFirstLogin = false; - + @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user") private Set authorities = new HashSet<>(); @@ -50,85 +51,83 @@ public class User { @MapKeyColumn(name = "setting_key") @Column(name = "setting_value") @CollectionTable(name = "user_settings", joinColumns = @JoinColumn(name = "user_id")) - private Map settings = new HashMap<>(); // Key-value pairs of settings. + private Map settings = new HashMap<>(); // Key-value pairs of settings. - - public boolean isFirstLogin() { - return isFirstLogin != null && isFirstLogin; - } + public boolean isFirstLogin() { + return isFirstLogin != null && isFirstLogin; + } - public void setFirstLogin(boolean isFirstLogin) { - this.isFirstLogin = isFirstLogin; - } + public void setFirstLogin(boolean isFirstLogin) { + this.isFirstLogin = isFirstLogin; + } - public Long getId() { - return id; - } + public Long getId() { + return id; + } - public void setId(Long id) { - this.id = id; - } + public void setId(Long id) { + this.id = id; + } - public String getApiKey() { - return apiKey; - } + public String getApiKey() { + return apiKey; + } - public void setApiKey(String apiKey) { - this.apiKey = apiKey; - } + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } - public Map getSettings() { - return settings; - } + public Map getSettings() { + return settings; + } - public void setSettings(Map settings) { - this.settings = settings; - } + public void setSettings(Map settings) { + this.settings = settings; + } - public String getUsername() { - return username; - } + public String getUsername() { + return username; + } - public void setUsername(String username) { - this.username = username; - } + public void setUsername(String username) { + this.username = username; + } - public String getPassword() { - return password; - } + public String getPassword() { + return password; + } - public void setPassword(String password) { - this.password = password; - } + public void setPassword(String password) { + this.password = password; + } - public boolean isEnabled() { - return enabled; - } + public boolean isEnabled() { + return enabled; + } - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } - public Set getAuthorities() { - return authorities; - } + public Set getAuthorities() { + return authorities; + } - public void setAuthorities(Set authorities) { - this.authorities = authorities; - } - - public void addAuthorities(Set authorities) { - this.authorities.addAll(authorities); - } - public void addAuthority(Authority authorities) { - this.authorities.add(authorities); - } - - public String getRolesAsString() { - return this.authorities.stream() - .map(Authority::getAuthority) - .collect(Collectors.joining(", ")); - } + public void setAuthorities(Set authorities) { + this.authorities = authorities; + } + public void addAuthorities(Set authorities) { + this.authorities.addAll(authorities); + } + public void addAuthority(Authority authorities) { + this.authorities.add(authorities); + } + + public String getRolesAsString() { + return this.authorities.stream() + .map(Authority::getAuthority) + .collect(Collectors.joining(", ")); + } } diff --git a/src/main/java/stirling/software/SPDF/model/api/GeneralFile.java b/src/main/java/stirling/software/SPDF/model/api/GeneralFile.java index 441d904a..1c0581cd 100644 --- a/src/main/java/stirling/software/SPDF/model/api/GeneralFile.java +++ b/src/main/java/stirling/software/SPDF/model/api/GeneralFile.java @@ -3,6 +3,7 @@ package stirling.software.SPDF.model.api; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -12,6 +13,6 @@ import lombok.NoArgsConstructor; @NoArgsConstructor public class GeneralFile { - @Schema(description = "The input file") - private MultipartFile fileInput; + @Schema(description = "The input file") + private MultipartFile fileInput; } diff --git a/src/main/java/stirling/software/SPDF/model/api/HandleDataRequest.java b/src/main/java/stirling/software/SPDF/model/api/HandleDataRequest.java index d830ffeb..d4a4a6bd 100644 --- a/src/main/java/stirling/software/SPDF/model/api/HandleDataRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/HandleDataRequest.java @@ -3,6 +3,7 @@ package stirling.software.SPDF.model.api; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; diff --git a/src/main/java/stirling/software/SPDF/model/api/ImageFile.java b/src/main/java/stirling/software/SPDF/model/api/ImageFile.java index 02079843..fdc0e6dc 100644 --- a/src/main/java/stirling/software/SPDF/model/api/ImageFile.java +++ b/src/main/java/stirling/software/SPDF/model/api/ImageFile.java @@ -3,6 +3,7 @@ package stirling.software.SPDF.model.api; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -11,6 +12,6 @@ import lombok.NoArgsConstructor; @NoArgsConstructor @EqualsAndHashCode public class ImageFile { - @Schema(description = "The input image file") + @Schema(description = "The input image file") private MultipartFile fileInput; } diff --git a/src/main/java/stirling/software/SPDF/model/api/MultiplePDFFiles.java b/src/main/java/stirling/software/SPDF/model/api/MultiplePDFFiles.java index 937a4265..00a34b74 100644 --- a/src/main/java/stirling/software/SPDF/model/api/MultiplePDFFiles.java +++ b/src/main/java/stirling/software/SPDF/model/api/MultiplePDFFiles.java @@ -3,13 +3,15 @@ package stirling.software.SPDF.model.api; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; + @Data @NoArgsConstructor @EqualsAndHashCode public class MultiplePDFFiles { - @Schema(description = "The input PDF files", type = "array", format = "binary") + @Schema(description = "The input PDF files", type = "array", format = "binary") private MultipartFile[] fileInput; } diff --git a/src/main/java/stirling/software/SPDF/model/api/PDFComparison.java b/src/main/java/stirling/software/SPDF/model/api/PDFComparison.java index 1f902d88..47377188 100644 --- a/src/main/java/stirling/software/SPDF/model/api/PDFComparison.java +++ b/src/main/java/stirling/software/SPDF/model/api/PDFComparison.java @@ -1,16 +1,18 @@ package stirling.software.SPDF.model.api; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; + @Data @NoArgsConstructor -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PDFComparison extends PDFFile { - - @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = { - "Greater", "Equal", "Less" }) + + @Schema( + description = "The comparison type, accepts Greater, Equal, Less than", + allowableValues = {"Greater", "Equal", "Less"}) private String comparator; - } diff --git a/src/main/java/stirling/software/SPDF/model/api/PDFComparisonAndCount.java b/src/main/java/stirling/software/SPDF/model/api/PDFComparisonAndCount.java index 14462f0a..04042bd8 100644 --- a/src/main/java/stirling/software/SPDF/model/api/PDFComparisonAndCount.java +++ b/src/main/java/stirling/software/SPDF/model/api/PDFComparisonAndCount.java @@ -1,15 +1,15 @@ package stirling.software.SPDF.model.api; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; + @Data @NoArgsConstructor -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PDFComparisonAndCount extends PDFComparison { - @Schema(description = "Count") + @Schema(description = "Count") private String pageCount; - - } diff --git a/src/main/java/stirling/software/SPDF/model/api/PDFFile.java b/src/main/java/stirling/software/SPDF/model/api/PDFFile.java index 378b3c03..68562190 100644 --- a/src/main/java/stirling/software/SPDF/model/api/PDFFile.java +++ b/src/main/java/stirling/software/SPDF/model/api/PDFFile.java @@ -3,11 +3,13 @@ package stirling.software.SPDF.model.api; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; + @Data @EqualsAndHashCode public class PDFFile { - @Schema(description = "The input PDF file") + @Schema(description = "The input PDF file") private MultipartFile fileInput; } diff --git a/src/main/java/stirling/software/SPDF/model/api/PDFWithImageFormatRequest.java b/src/main/java/stirling/software/SPDF/model/api/PDFWithImageFormatRequest.java index aa8fe08b..34f15c13 100644 --- a/src/main/java/stirling/software/SPDF/model/api/PDFWithImageFormatRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/PDFWithImageFormatRequest.java @@ -1,14 +1,16 @@ package stirling.software.SPDF.model.api; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PDFWithImageFormatRequest extends PDFFile { - @Schema(description = "The output image format e.g., 'png', 'jpeg', or 'gif'", - allowableValues = { "png", "jpeg", "gif" }) + @Schema( + description = "The output image format e.g., 'png', 'jpeg', or 'gif'", + allowableValues = {"png", "jpeg", "gif"}) private String format; } diff --git a/src/main/java/stirling/software/SPDF/model/api/PDFWithPageNums.java b/src/main/java/stirling/software/SPDF/model/api/PDFWithPageNums.java index 34542da8..aa664be0 100644 --- a/src/main/java/stirling/software/SPDF/model/api/PDFWithPageNums.java +++ b/src/main/java/stirling/software/SPDF/model/api/PDFWithPageNums.java @@ -7,36 +7,38 @@ import org.apache.pdfbox.pdmodel.PDDocument; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import stirling.software.SPDF.utils.GeneralUtils; + @Data @NoArgsConstructor -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PDFWithPageNums extends PDFFile { - - @Schema(description = "The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')\"") + + @Schema( + description = + "The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')\"") private String pageNumbers; - - @Hidden - public List getPageNumbersList(){ - int pageCount = 0; - try { - pageCount = PDDocument.load(getFileInput().getInputStream()).getNumberOfPages(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - return GeneralUtils.parsePageString(pageNumbers, pageCount); - - } - - @Hidden - public List getPageNumbersList(PDDocument doc){ - int pageCount = 0; - pageCount = doc.getNumberOfPages(); - return GeneralUtils.parsePageString(pageNumbers, pageCount); - - } + + @Hidden + public List getPageNumbersList() { + int pageCount = 0; + try { + pageCount = PDDocument.load(getFileInput().getInputStream()).getNumberOfPages(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return GeneralUtils.parsePageString(pageNumbers, pageCount); + } + + @Hidden + public List getPageNumbersList(PDDocument doc) { + int pageCount = 0; + pageCount = doc.getNumberOfPages(); + return GeneralUtils.parsePageString(pageNumbers, pageCount); + } } diff --git a/src/main/java/stirling/software/SPDF/model/api/PDFWithPageSize.java b/src/main/java/stirling/software/SPDF/model/api/PDFWithPageSize.java index 661a4ffe..139f492d 100644 --- a/src/main/java/stirling/software/SPDF/model/api/PDFWithPageSize.java +++ b/src/main/java/stirling/software/SPDF/model/api/PDFWithPageSize.java @@ -1,16 +1,17 @@ package stirling.software.SPDF.model.api; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PDFWithPageSize extends PDFFile { - @Schema(description = "The scale of pages in the output PDF. Acceptable values are A0-A6, LETTER, LEGAL.", - allowableValues = { - "A0", "A1", "A2", "A3", "A4", "A5", "A6", "LETTER", "LEGAL" - }) - private String pageSize; + @Schema( + description = + "The scale of pages in the output PDF. Acceptable values are A0-A6, LETTER, LEGAL.", + allowableValues = {"A0", "A1", "A2", "A3", "A4", "A5", "A6", "LETTER", "LEGAL"}) + private String pageSize; } diff --git a/src/main/java/stirling/software/SPDF/model/api/SplitPdfBySectionsRequest.java b/src/main/java/stirling/software/SPDF/model/api/SplitPdfBySectionsRequest.java index 14112152..fcc5d5ba 100644 --- a/src/main/java/stirling/software/SPDF/model/api/SplitPdfBySectionsRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/SplitPdfBySectionsRequest.java @@ -1,14 +1,16 @@ package stirling.software.SPDF.model.api; + import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @Data @NoArgsConstructor -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class SplitPdfBySectionsRequest extends PDFFile { - @Schema(description = "Number of horizontal divisions for each PDF page", example = "2") + @Schema(description = "Number of horizontal divisions for each PDF page", example = "2") private int horizontalDivisions; @Schema(description = "Number of vertical divisions for each PDF page", example = "2") diff --git a/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToImageRequest.java b/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToImageRequest.java index 18026618..eaa8e361 100644 --- a/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToImageRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToImageRequest.java @@ -1,21 +1,29 @@ package stirling.software.SPDF.model.api.converters; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class ConvertToImageRequest extends PDFFile { - @Schema(description = "The output image format", allowableValues = {"png", "jpeg", "jpg", "gif"}) + @Schema( + description = "The output image format", + allowableValues = {"png", "jpeg", "jpg", "gif"}) private String imageFormat; - @Schema(description = "Choose between a single image containing all pages or separate images for each page", allowableValues = {"single", "multiple"}) + @Schema( + description = + "Choose between a single image containing all pages or separate images for each page", + allowableValues = {"single", "multiple"}) private String singleOrMultiple; - @Schema(description = "The color type of the output image(s)", allowableValues = {"color", "greyscale", "blackwhite"}) + @Schema( + description = "The color type of the output image(s)", + allowableValues = {"color", "greyscale", "blackwhite"}) private String colorType; @Schema(description = "The DPI (dots per inch) for the output image(s)") diff --git a/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToPdfRequest.java b/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToPdfRequest.java index 37df6f9e..7630f746 100644 --- a/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToPdfRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToPdfRequest.java @@ -3,6 +3,7 @@ package stirling.software.SPDF.model.api.converters; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; @@ -13,15 +14,18 @@ public class ConvertToPdfRequest { @Schema(description = "The input images to be converted to a PDF file") private MultipartFile[] fileInput; - @Schema(description = "Option to determine how the image will fit onto the page", - allowableValues = { "fillPage", "fitDocumentToImage", "maintainAspectRatio" }) + @Schema( + description = "Option to determine how the image will fit onto the page", + allowableValues = {"fillPage", "fitDocumentToImage", "maintainAspectRatio"}) private String fitOption; - - - @Schema(description = "The color type of the output image(s)", allowableValues = {"color", "greyscale", "blackwhite"}) + @Schema( + description = "The color type of the output image(s)", + allowableValues = {"color", "greyscale", "blackwhite"}) private String colorType; - @Schema(description = "Whether to automatically rotate the images to better fit the PDF page", example = "true") + @Schema( + description = "Whether to automatically rotate the images to better fit the PDF page", + example = "true") private boolean autoRotate; } diff --git a/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPresentationRequest.java b/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPresentationRequest.java index 0e8b79ad..45064375 100644 --- a/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPresentationRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPresentationRequest.java @@ -1,14 +1,17 @@ package stirling.software.SPDF.model.api.converters; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PdfToPresentationRequest extends PDFFile { - @Schema(description = "The output Presentation format", allowableValues = {"ppt", "pptx", "odp"}) + @Schema( + description = "The output Presentation format", + allowableValues = {"ppt", "pptx", "odp"}) private String outputFormat; } diff --git a/src/main/java/stirling/software/SPDF/model/api/converters/PdfToTextOrRTFRequest.java b/src/main/java/stirling/software/SPDF/model/api/converters/PdfToTextOrRTFRequest.java index 687ed621..5759d332 100644 --- a/src/main/java/stirling/software/SPDF/model/api/converters/PdfToTextOrRTFRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/converters/PdfToTextOrRTFRequest.java @@ -1,14 +1,17 @@ package stirling.software.SPDF.model.api.converters; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PdfToTextOrRTFRequest extends PDFFile { - @Schema(description = "The output Text or RTF format", allowableValues = {"rtf", "txt:Text"}) + @Schema( + description = "The output Text or RTF format", + allowableValues = {"rtf", "txt:Text"}) private String outputFormat; } diff --git a/src/main/java/stirling/software/SPDF/model/api/converters/PdfToWordRequest.java b/src/main/java/stirling/software/SPDF/model/api/converters/PdfToWordRequest.java index 87150c73..db3c3dfb 100644 --- a/src/main/java/stirling/software/SPDF/model/api/converters/PdfToWordRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/converters/PdfToWordRequest.java @@ -1,14 +1,17 @@ package stirling.software.SPDF.model.api.converters; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PdfToWordRequest extends PDFFile { - @Schema(description = "The output Word document format", allowableValues = {"doc", "docx", "odt"}) + @Schema( + description = "The output Word document format", + allowableValues = {"doc", "docx", "odt"}) private String outputFormat; } diff --git a/src/main/java/stirling/software/SPDF/model/api/converters/UrlToPdfRequest.java b/src/main/java/stirling/software/SPDF/model/api/converters/UrlToPdfRequest.java index 4607c153..3e340868 100644 --- a/src/main/java/stirling/software/SPDF/model/api/converters/UrlToPdfRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/converters/UrlToPdfRequest.java @@ -1,6 +1,7 @@ package stirling.software.SPDF.model.api.converters; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/src/main/java/stirling/software/SPDF/model/api/extract/PDFFilePage.java b/src/main/java/stirling/software/SPDF/model/api/extract/PDFFilePage.java index bfe87a16..faf955c6 100644 --- a/src/main/java/stirling/software/SPDF/model/api/extract/PDFFilePage.java +++ b/src/main/java/stirling/software/SPDF/model/api/extract/PDFFilePage.java @@ -1,18 +1,15 @@ package stirling.software.SPDF.model.api.extract; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PDFFilePage extends PDFFile { - @Schema(description = "Number of chosen page", type = "number") private int pageId; - - } - diff --git a/src/main/java/stirling/software/SPDF/model/api/filter/ContainsTextRequest.java b/src/main/java/stirling/software/SPDF/model/api/filter/ContainsTextRequest.java index 0b6cb1cb..7cd2f76a 100644 --- a/src/main/java/stirling/software/SPDF/model/api/filter/ContainsTextRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/filter/ContainsTextRequest.java @@ -1,12 +1,13 @@ package stirling.software.SPDF.model.api.filter; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFWithPageNums; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class ContainsTextRequest extends PDFWithPageNums { @Schema(description = "The text to check for", required = true) diff --git a/src/main/java/stirling/software/SPDF/model/api/filter/FileSizeRequest.java b/src/main/java/stirling/software/SPDF/model/api/filter/FileSizeRequest.java index ce9a9236..00d75139 100644 --- a/src/main/java/stirling/software/SPDF/model/api/filter/FileSizeRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/filter/FileSizeRequest.java @@ -1,16 +1,15 @@ package stirling.software.SPDF.model.api.filter; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFComparison; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class FileSizeRequest extends PDFComparison { @Schema(description = "File Size", required = true) private String fileSize; - - } diff --git a/src/main/java/stirling/software/SPDF/model/api/filter/PageRotationRequest.java b/src/main/java/stirling/software/SPDF/model/api/filter/PageRotationRequest.java index d5fb9739..949178c5 100644 --- a/src/main/java/stirling/software/SPDF/model/api/filter/PageRotationRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/filter/PageRotationRequest.java @@ -1,15 +1,15 @@ package stirling.software.SPDF.model.api.filter; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFComparison; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PageRotationRequest extends PDFComparison { @Schema(description = "Rotation in degrees", required = true) private int rotation; - } diff --git a/src/main/java/stirling/software/SPDF/model/api/filter/PageSizeRequest.java b/src/main/java/stirling/software/SPDF/model/api/filter/PageSizeRequest.java index 12083636..c8b46a90 100644 --- a/src/main/java/stirling/software/SPDF/model/api/filter/PageSizeRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/filter/PageSizeRequest.java @@ -1,16 +1,15 @@ package stirling.software.SPDF.model.api.filter; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFComparison; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PageSizeRequest extends PDFComparison { @Schema(description = "Standard Page Size", required = true) private String standardPageSize; - - } diff --git a/src/main/java/stirling/software/SPDF/model/api/general/CropPdfForm.java b/src/main/java/stirling/software/SPDF/model/api/general/CropPdfForm.java index 52821515..cec72ce4 100644 --- a/src/main/java/stirling/software/SPDF/model/api/general/CropPdfForm.java +++ b/src/main/java/stirling/software/SPDF/model/api/general/CropPdfForm.java @@ -1,18 +1,23 @@ package stirling.software.SPDF.model.api.general; + import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class CropPdfForm extends PDFFile { - - @Schema(description = "The x-coordinate of the top-left corner of the crop area", type = "number") + @Schema( + description = "The x-coordinate of the top-left corner of the crop area", + type = "number") private float x; - @Schema(description = "The y-coordinate of the top-left corner of the crop area", type = "number") + @Schema( + description = "The y-coordinate of the top-left corner of the crop area", + type = "number") private float y; @Schema(description = "The width of the crop area", type = "number") @@ -21,4 +26,3 @@ public class CropPdfForm extends PDFFile { @Schema(description = "The height of the crop area", type = "number") private float height; } - diff --git a/src/main/java/stirling/software/SPDF/model/api/general/MergeMultiplePagesRequest.java b/src/main/java/stirling/software/SPDF/model/api/general/MergeMultiplePagesRequest.java index 4642cb75..1ecdc2ee 100644 --- a/src/main/java/stirling/software/SPDF/model/api/general/MergeMultiplePagesRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/general/MergeMultiplePagesRequest.java @@ -1,18 +1,21 @@ package stirling.software.SPDF.model.api.general; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class MergeMultiplePagesRequest extends PDFFile { - @Schema(description = "The number of pages to fit onto a single sheet in the output PDF.", - type = "integer", allowableValues = {"2", "3", "4", "9", "16"}) + @Schema( + description = "The number of pages to fit onto a single sheet in the output PDF.", + type = "integer", + allowableValues = {"2", "3", "4", "9", "16"}) private int pagesPerSheet; - + @Schema(description = "Boolean for if you wish to add border around the pages") private boolean addBorder; } diff --git a/src/main/java/stirling/software/SPDF/model/api/general/MergePdfsRequest.java b/src/main/java/stirling/software/SPDF/model/api/general/MergePdfsRequest.java index b7b3bda7..0d0a98ae 100644 --- a/src/main/java/stirling/software/SPDF/model/api/general/MergePdfsRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/general/MergePdfsRequest.java @@ -1,22 +1,24 @@ package stirling.software.SPDF.model.api.general; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.MultiplePDFFiles; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class MergePdfsRequest extends MultiplePDFFiles { - - @Schema(description = "The type of sorting to be applied on the input files before merging.", + + @Schema( + description = "The type of sorting to be applied on the input files before merging.", allowableValues = { - "orderProvided", - "byFileName", - "byDateModified", - "byDateCreated", + "orderProvided", + "byFileName", + "byDateModified", + "byDateCreated", "byPDFTitle" - }, + }, defaultValue = "orderProvided") private String sortType = "orderProvided"; } diff --git a/src/main/java/stirling/software/SPDF/model/api/general/OverlayPdfsRequest.java b/src/main/java/stirling/software/SPDF/model/api/general/OverlayPdfsRequest.java index 13ca7857..458dd699 100644 --- a/src/main/java/stirling/software/SPDF/model/api/general/OverlayPdfsRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/general/OverlayPdfsRequest.java @@ -1,24 +1,34 @@ package stirling.software.SPDF.model.api.general; + import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data @EqualsAndHashCode(callSuper = true) -public class OverlayPdfsRequest extends PDFFile { +public class OverlayPdfsRequest extends PDFFile { - @Schema(description = "An array of PDF files to be used as overlays on the base PDF. The order in these files is applied based on the selected mode.") + @Schema( + description = + "An array of PDF files to be used as overlays on the base PDF. The order in these files is applied based on the selected mode.") private MultipartFile[] overlayFiles; - @Schema(description = "The mode of overlaying: 'SequentialOverlay' for sequential application, 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay' for fixed repetition based on provided counts", required = true) + @Schema( + description = + "The mode of overlaying: 'SequentialOverlay' for sequential application, 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay' for fixed repetition based on provided counts", + required = true) private String overlayMode; - @Schema(description = "An array of integers specifying the number of times each corresponding overlay file should be applied in the 'FixedRepeatOverlay' mode. This should match the length of the overlayFiles array.", required = false) + @Schema( + description = + "An array of integers specifying the number of times each corresponding overlay file should be applied in the 'FixedRepeatOverlay' mode. This should match the length of the overlayFiles array.", + required = false) private int[] counts; - + @Schema(description = "Overlay position 0 is Foregound, 1 is Background") private int overlayPosition; } diff --git a/src/main/java/stirling/software/SPDF/model/api/general/RearrangePagesRequest.java b/src/main/java/stirling/software/SPDF/model/api/general/RearrangePagesRequest.java index 3e5b4f23..b20ced47 100644 --- a/src/main/java/stirling/software/SPDF/model/api/general/RearrangePagesRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/general/RearrangePagesRequest.java @@ -1,23 +1,26 @@ package stirling.software.SPDF.model.api.general; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.SortTypes; import stirling.software.SPDF.model.api.PDFWithPageNums; + @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class RearrangePagesRequest extends PDFWithPageNums { - @Schema(implementation = SortTypes.class, description = "The custom mode for page rearrangement. Valid values are:\n" - + "REVERSE_ORDER: Reverses the order of all pages.\n" - + "DUPLEX_SORT: Sorts pages as if all fronts were scanned then all backs in reverse (1, n, 2, n-1, ...). " - + "BOOKLET_SORT: Arranges pages for booklet printing (last, first, second, second last, ...).\n" - + "ODD_EVEN_SPLIT: Splits and arranges pages into odd and even numbered pages.\n" - + "REMOVE_FIRST: Removes the first page.\n" + "REMOVE_LAST: Removes the last page.\n" - + "REMOVE_FIRST_AND_LAST: Removes both the first and the last pages.\n") + @Schema( + implementation = SortTypes.class, + description = + "The custom mode for page rearrangement. Valid values are:\n" + + "REVERSE_ORDER: Reverses the order of all pages.\n" + + "DUPLEX_SORT: Sorts pages as if all fronts were scanned then all backs in reverse (1, n, 2, n-1, ...). " + + "BOOKLET_SORT: Arranges pages for booklet printing (last, first, second, second last, ...).\n" + + "ODD_EVEN_SPLIT: Splits and arranges pages into odd and even numbered pages.\n" + + "REMOVE_FIRST: Removes the first page.\n" + + "REMOVE_LAST: Removes the last page.\n" + + "REMOVE_FIRST_AND_LAST: Removes both the first and the last pages.\n") private String customMode; - - - } diff --git a/src/main/java/stirling/software/SPDF/model/api/general/RotatePDFRequest.java b/src/main/java/stirling/software/SPDF/model/api/general/RotatePDFRequest.java index 8f48c605..1efd7049 100644 --- a/src/main/java/stirling/software/SPDF/model/api/general/RotatePDFRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/general/RotatePDFRequest.java @@ -1,14 +1,18 @@ package stirling.software.SPDF.model.api.general; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class RotatePDFRequest extends PDFFile { - @Schema(description = "The angle by which to rotate the PDF file. This should be a multiple of 90.", example = "90") + @Schema( + description = + "The angle by which to rotate the PDF file. This should be a multiple of 90.", + example = "90") private Integer angle; } diff --git a/src/main/java/stirling/software/SPDF/model/api/general/ScalePagesRequest.java b/src/main/java/stirling/software/SPDF/model/api/general/ScalePagesRequest.java index ff44b01d..0ba004c9 100644 --- a/src/main/java/stirling/software/SPDF/model/api/general/ScalePagesRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/general/ScalePagesRequest.java @@ -1,14 +1,17 @@ package stirling.software.SPDF.model.api.general; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFWithPageSize; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class ScalePagesRequest extends PDFWithPageSize { - @Schema(description = "The scale of the content on the pages of the output PDF. Acceptable values are floats.") + @Schema( + description = + "The scale of the content on the pages of the output PDF. Acceptable values are floats.") private float scaleFactor; } diff --git a/src/main/java/stirling/software/SPDF/model/api/general/SplitPdfBySizeOrCountRequest.java b/src/main/java/stirling/software/SPDF/model/api/general/SplitPdfBySizeOrCountRequest.java index 087ce80c..8b36c1c8 100644 --- a/src/main/java/stirling/software/SPDF/model/api/general/SplitPdfBySizeOrCountRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/general/SplitPdfBySizeOrCountRequest.java @@ -1,18 +1,26 @@ package stirling.software.SPDF.model.api.general; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class SplitPdfBySizeOrCountRequest extends PDFFile { - @Schema(description = "Determines the type of split: 0 for size, 1 for page count, 2 for document count", required = false, defaultValue = "0") - private int splitType; + @Schema( + description = + "Determines the type of split: 0 for size, 1 for page count, 2 for document count", + required = false, + defaultValue = "0") + private int splitType; - - @Schema(description = "Value for split: size in MB (e.g., '10MB') or number of pages (e.g., '5')", required = false, defaultValue = "10MB") + @Schema( + description = + "Value for split: size in MB (e.g., '10MB') or number of pages (e.g., '5')", + required = false, + defaultValue = "10MB") private String splitValue; } diff --git a/src/main/java/stirling/software/SPDF/model/api/misc/AddPageNumbersRequest.java b/src/main/java/stirling/software/SPDF/model/api/misc/AddPageNumbersRequest.java index 313d42ab..5622903a 100644 --- a/src/main/java/stirling/software/SPDF/model/api/misc/AddPageNumbersRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/misc/AddPageNumbersRequest.java @@ -1,15 +1,18 @@ package stirling.software.SPDF.model.api.misc; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFWithPageNums; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class AddPageNumbersRequest extends PDFWithPageNums { - @Schema(description = "Custom margin: small/medium/large", allowableValues = {"small", "medium", "large"}) + @Schema( + description = "Custom margin: small/medium/large", + allowableValues = {"small", "medium", "large"}) private String customMargin; @Schema(description = "Position: 1 of 9 positions", minimum = "1", maximum = "9") @@ -21,6 +24,8 @@ public class AddPageNumbersRequest extends PDFWithPageNums { @Schema(description = "Which pages to number, default all") private String pagesToNumber; - @Schema(description = "Custom text: defaults to just number but can have things like \"Page {n} of {p}\"") + @Schema( + description = + "Custom text: defaults to just number but can have things like \"Page {n} of {p}\"") private String customText; } diff --git a/src/main/java/stirling/software/SPDF/model/api/misc/AutoSplitPdfRequest.java b/src/main/java/stirling/software/SPDF/model/api/misc/AutoSplitPdfRequest.java index c4923746..c60d50e8 100644 --- a/src/main/java/stirling/software/SPDF/model/api/misc/AutoSplitPdfRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/misc/AutoSplitPdfRequest.java @@ -1,14 +1,19 @@ package stirling.software.SPDF.model.api.misc; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class AutoSplitPdfRequest extends PDFFile { - @Schema(description = "Flag indicating if the duplex mode is active, where the page after the divider also gets removed.", required = false, defaultValue = "false") + @Schema( + description = + "Flag indicating if the duplex mode is active, where the page after the divider also gets removed.", + required = false, + defaultValue = "false") private boolean duplexMode; } diff --git a/src/main/java/stirling/software/SPDF/model/api/misc/ExtractHeaderRequest.java b/src/main/java/stirling/software/SPDF/model/api/misc/ExtractHeaderRequest.java index f3028445..33c941af 100644 --- a/src/main/java/stirling/software/SPDF/model/api/misc/ExtractHeaderRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/misc/ExtractHeaderRequest.java @@ -1,14 +1,19 @@ package stirling.software.SPDF.model.api.misc; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class ExtractHeaderRequest extends PDFFile { - @Schema(description = "Flag indicating whether to use the first text as a fallback if no suitable title is found. Defaults to false.", required = false, defaultValue = "false") + @Schema( + description = + "Flag indicating whether to use the first text as a fallback if no suitable title is found. Defaults to false.", + required = false, + defaultValue = "false") private boolean useFirstTextAsFallback; } diff --git a/src/main/java/stirling/software/SPDF/model/api/misc/ExtractImageScansRequest.java b/src/main/java/stirling/software/SPDF/model/api/misc/ExtractImageScansRequest.java index 1a575fe6..6839451e 100644 --- a/src/main/java/stirling/software/SPDF/model/api/misc/ExtractImageScansRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/misc/ExtractImageScansRequest.java @@ -3,6 +3,7 @@ package stirling.software.SPDF.model.api.misc; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; @@ -12,18 +13,33 @@ public class ExtractImageScansRequest { @Schema(description = "The input file containing image scans", required = true) private MultipartFile fileInput; - @Schema(description = "The angle threshold for the image scan extraction", defaultValue = "5", example = "5") + @Schema( + description = "The angle threshold for the image scan extraction", + defaultValue = "5", + example = "5") private int angleThreshold = 5; - @Schema(description = "The tolerance for the image scan extraction", defaultValue = "20", example = "20") + @Schema( + description = "The tolerance for the image scan extraction", + defaultValue = "20", + example = "20") private int tolerance = 20; - @Schema(description = "The minimum area for the image scan extraction", defaultValue = "8000", example = "8000") + @Schema( + description = "The minimum area for the image scan extraction", + defaultValue = "8000", + example = "8000") private int minArea = 8000; - @Schema(description = "The minimum contour area for the image scan extraction", defaultValue = "500", example = "500") + @Schema( + description = "The minimum contour area for the image scan extraction", + defaultValue = "500", + example = "500") private int minContourArea = 500; - @Schema(description = "The border size for the image scan extraction", defaultValue = "1", example = "1") - private int borderSize =1; + @Schema( + description = "The border size for the image scan extraction", + defaultValue = "1", + example = "1") + private int borderSize = 1; } diff --git a/src/main/java/stirling/software/SPDF/model/api/misc/MetadataRequest.java b/src/main/java/stirling/software/SPDF/model/api/misc/MetadataRequest.java index d62890aa..e638d7d2 100644 --- a/src/main/java/stirling/software/SPDF/model/api/misc/MetadataRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/misc/MetadataRequest.java @@ -3,12 +3,13 @@ package stirling.software.SPDF.model.api.misc; import java.util.Map; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class MetadataRequest extends PDFFile { @Schema(description = "Delete all metadata if set to true") @@ -41,6 +42,8 @@ public class MetadataRequest extends PDFFile { @Schema(description = "The trapped status of the document") private String trapped; - @Schema(description = "Map list of key and value of custom parameters. Note these must start with customKey and customValue if they are non-standard") + @Schema( + description = + "Map list of key and value of custom parameters. Note these must start with customKey and customValue if they are non-standard") private Map allRequestParams; } diff --git a/src/main/java/stirling/software/SPDF/model/api/misc/OptimizePdfRequest.java b/src/main/java/stirling/software/SPDF/model/api/misc/OptimizePdfRequest.java index bc00cf20..96a787f3 100644 --- a/src/main/java/stirling/software/SPDF/model/api/misc/OptimizePdfRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/misc/OptimizePdfRequest.java @@ -1,16 +1,19 @@ package stirling.software.SPDF.model.api.misc; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class OptimizePdfRequest extends PDFFile { - @Schema(description = "The level of optimization to apply to the PDF file. Higher values indicate greater compression but may reduce quality.", - allowableValues = { "1", "2", "3", "4", "5" }) + @Schema( + description = + "The level of optimization to apply to the PDF file. Higher values indicate greater compression but may reduce quality.", + allowableValues = {"1", "2", "3", "4", "5"}) private Integer optimizeLevel; @Schema(description = "The expected output size, e.g. '100MB', '25KB', etc.") diff --git a/src/main/java/stirling/software/SPDF/model/api/misc/OverlayImageRequest.java b/src/main/java/stirling/software/SPDF/model/api/misc/OverlayImageRequest.java index 50ec4abb..b057709c 100644 --- a/src/main/java/stirling/software/SPDF/model/api/misc/OverlayImageRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/misc/OverlayImageRequest.java @@ -3,23 +3,30 @@ package stirling.software.SPDF.model.api.misc; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class OverlayImageRequest extends PDFFile { @Schema(description = "The image file to be overlaid onto the PDF.") private MultipartFile imageFile; - @Schema(description = "The x-coordinate at which to place the top-left corner of the image.", example = "0") + @Schema( + description = "The x-coordinate at which to place the top-left corner of the image.", + example = "0") private float x; - @Schema(description = "The y-coordinate at which to place the top-left corner of the image.", example = "0") + @Schema( + description = "The y-coordinate at which to place the top-left corner of the image.", + example = "0") private float y; - @Schema(description = "Whether to overlay the image onto every page of the PDF.", example = "false") + @Schema( + description = "Whether to overlay the image onto every page of the PDF.", + example = "false") private boolean everyPage; } diff --git a/src/main/java/stirling/software/SPDF/model/api/misc/ProcessPdfWithOcrRequest.java b/src/main/java/stirling/software/SPDF/model/api/misc/ProcessPdfWithOcrRequest.java index 392f8d54..7d3de3e6 100644 --- a/src/main/java/stirling/software/SPDF/model/api/misc/ProcessPdfWithOcrRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/misc/ProcessPdfWithOcrRequest.java @@ -3,12 +3,13 @@ package stirling.software.SPDF.model.api.misc; import java.util.List; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class ProcessPdfWithOcrRequest extends PDFFile { @Schema(description = "List of languages to use in OCR processing") @@ -26,10 +27,15 @@ public class ProcessPdfWithOcrRequest extends PDFFile { @Schema(description = "Clean the final output if set to true") private boolean cleanFinal; - @Schema(description = "Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'", allowableValues = {"skip-text", "force-ocr", "Normal"}) + @Schema( + description = "Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'", + allowableValues = {"skip-text", "force-ocr", "Normal"}) private String ocrType; - @Schema(description = "Specify the OCR render type, either 'hocr' or 'sandwich'", allowableValues = {"hocr", "sandwich"}, defaultValue = "hocr") + @Schema( + description = "Specify the OCR render type, either 'hocr' or 'sandwich'", + allowableValues = {"hocr", "sandwich"}, + defaultValue = "hocr") private String ocrRenderType = "hocr"; @Schema(description = "Remove images from the output PDF if set to true") diff --git a/src/main/java/stirling/software/SPDF/model/api/misc/RemoveBlankPagesRequest.java b/src/main/java/stirling/software/SPDF/model/api/misc/RemoveBlankPagesRequest.java index 0d2e11c7..7177449f 100644 --- a/src/main/java/stirling/software/SPDF/model/api/misc/RemoveBlankPagesRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/misc/RemoveBlankPagesRequest.java @@ -1,17 +1,24 @@ package stirling.software.SPDF.model.api.misc; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class RemoveBlankPagesRequest extends PDFFile { - @Schema(description = "The threshold value to determine blank pages", example = "10", defaultValue = "10") + @Schema( + description = "The threshold value to determine blank pages", + example = "10", + defaultValue = "10") private int threshold = 10; - @Schema(description = "The percentage of white color on a page to consider it as blank", example = "99.9", defaultValue = "99.9") + @Schema( + description = "The percentage of white color on a page to consider it as blank", + example = "99.9", + defaultValue = "99.9") private float whitePercent = 99.9f; } diff --git a/src/main/java/stirling/software/SPDF/model/api/security/AddPasswordRequest.java b/src/main/java/stirling/software/SPDF/model/api/security/AddPasswordRequest.java index ea83e470..99cdfc36 100644 --- a/src/main/java/stirling/software/SPDF/model/api/security/AddPasswordRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/security/AddPasswordRequest.java @@ -1,30 +1,44 @@ package stirling.software.SPDF.model.api.security; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class AddPasswordRequest extends PDFFile { - @Schema(description = "The owner password to be added to the PDF file (Restricts what can be done with the document once it is opened)", defaultValue = "") + @Schema( + description = + "The owner password to be added to the PDF file (Restricts what can be done with the document once it is opened)", + defaultValue = "") private String ownerPassword; - @Schema(description = "The password to be added to the PDF file (Restricts the opening of the document itself.)", defaultValue = "") + @Schema( + description = + "The password to be added to the PDF file (Restricts the opening of the document itself.)", + defaultValue = "") private String password; - @Schema(description = "The length of the encryption key", allowableValues = {"40", "128", "256"}, defaultValue = "256") + @Schema( + description = "The length of the encryption key", + allowableValues = {"40", "128", "256"}, + defaultValue = "256") private int keyLength = 256; @Schema(description = "Whether the document assembly is allowed", example = "false") private boolean canAssembleDocument; - @Schema(description = "Whether content extraction for accessibility is allowed", example = "false") + @Schema( + description = "Whether content extraction for accessibility is allowed", + example = "false") private boolean canExtractContent; - @Schema(description = "Whether content extraction for accessibility is allowed", example = "false") + @Schema( + description = "Whether content extraction for accessibility is allowed", + example = "false") private boolean canExtractForAccessibility; @Schema(description = "Whether form filling is allowed", example = "false") diff --git a/src/main/java/stirling/software/SPDF/model/api/security/AddWatermarkRequest.java b/src/main/java/stirling/software/SPDF/model/api/security/AddWatermarkRequest.java index cd800948..861568bf 100644 --- a/src/main/java/stirling/software/SPDF/model/api/security/AddWatermarkRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/security/AddWatermarkRequest.java @@ -3,17 +3,19 @@ package stirling.software.SPDF.model.api.security; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class AddWatermarkRequest extends PDFFile { - @Schema(description = "The watermark type (text or image)", - allowableValues = {"text", "image"}, - required = true) + @Schema( + description = "The watermark type (text or image)", + allowableValues = {"text", "image"}, + required = true) private String watermarkType; @Schema(description = "The watermark text") @@ -22,8 +24,9 @@ public class AddWatermarkRequest extends PDFFile { @Schema(description = "The watermark image") private MultipartFile watermarkImage; - @Schema(description = "The selected alphabet", - allowableValues = {"roman", "arabic", "japanese", "korean", "chinese"}, + @Schema( + description = "The selected alphabet", + allowableValues = {"roman", "arabic", "japanese", "korean", "chinese"}, defaultValue = "roman") private String alphabet = "roman"; diff --git a/src/main/java/stirling/software/SPDF/model/api/security/PDFPasswordRequest.java b/src/main/java/stirling/software/SPDF/model/api/security/PDFPasswordRequest.java index 94d04d1e..7f31abbb 100644 --- a/src/main/java/stirling/software/SPDF/model/api/security/PDFPasswordRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/security/PDFPasswordRequest.java @@ -1,12 +1,13 @@ package stirling.software.SPDF.model.api.security; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PDFPasswordRequest extends PDFFile { @Schema(description = "The password of the PDF file", required = true) diff --git a/src/main/java/stirling/software/SPDF/model/api/security/RedactPdfRequest.java b/src/main/java/stirling/software/SPDF/model/api/security/RedactPdfRequest.java index 1966c53a..fce57ff5 100644 --- a/src/main/java/stirling/software/SPDF/model/api/security/RedactPdfRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/security/RedactPdfRequest.java @@ -1,12 +1,13 @@ package stirling.software.SPDF.model.api.security; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class RedactPdfRequest extends PDFFile { @Schema(description = "List of text to redact from the PDF", type = "string", required = true) diff --git a/src/main/java/stirling/software/SPDF/model/api/security/SanitizePdfRequest.java b/src/main/java/stirling/software/SPDF/model/api/security/SanitizePdfRequest.java index 98c7743f..0e12dfe7 100644 --- a/src/main/java/stirling/software/SPDF/model/api/security/SanitizePdfRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/security/SanitizePdfRequest.java @@ -1,12 +1,13 @@ package stirling.software.SPDF.model.api.security; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class SanitizePdfRequest extends PDFFile { @Schema(description = "Remove JavaScript actions from the PDF", defaultValue = "false") diff --git a/src/main/java/stirling/software/SPDF/model/api/security/SignPDFWithCertRequest.java b/src/main/java/stirling/software/SPDF/model/api/security/SignPDFWithCertRequest.java index 8e537c6a..a1fc2fce 100644 --- a/src/main/java/stirling/software/SPDF/model/api/security/SignPDFWithCertRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/security/SignPDFWithCertRequest.java @@ -3,18 +3,23 @@ package stirling.software.SPDF.model.api.security; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class SignPDFWithCertRequest extends PDFFile { - @Schema(description = "The type of the digital certificate", allowableValues = { "PKCS12", "PEM" }) + @Schema( + description = "The type of the digital certificate", + allowableValues = {"PKCS12", "PEM"}) private String certType; - @Schema(description = "The private key for the digital certificate (required for PEM type certificates)") + @Schema( + description = + "The private key for the digital certificate (required for PEM type certificates)") private MultipartFile privateKeyFile; @Schema(description = "The digital certificate (required for PEM type certificates)") @@ -38,6 +43,8 @@ public class SignPDFWithCertRequest extends PDFFile { @Schema(description = "The name of the signer") private String name; - @Schema(description = "The page number where the signature should be visible. This is required if showSignature is set to true") + @Schema( + description = + "The page number where the signature should be visible. This is required if showSignature is set to true") private Integer pageNumber; } diff --git a/src/main/java/stirling/software/SPDF/pdf/ImageFinder.java b/src/main/java/stirling/software/SPDF/pdf/ImageFinder.java index 0f49af76..a710dbd5 100644 --- a/src/main/java/stirling/software/SPDF/pdf/ImageFinder.java +++ b/src/main/java/stirling/software/SPDF/pdf/ImageFinder.java @@ -48,83 +48,84 @@ public class ImageFinder extends org.apache.pdfbox.contentstream.PDFGraphicsStre super.processOperator(operator, operands); } - @Override - public void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3) throws IOException { - // TODO Auto-generated method stub - - } + @Override + public void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3) throws IOException { + // TODO Auto-generated method stub - @Override - public void drawImage(PDImage pdImage) throws IOException { - // TODO Auto-generated method stub - - } + } - @Override - public void clip(int windingRule) throws IOException { - // TODO Auto-generated method stub - - } + @Override + public void drawImage(PDImage pdImage) throws IOException { + // TODO Auto-generated method stub - @Override - public void moveTo(float x, float y) throws IOException { - // TODO Auto-generated method stub - - } + } - @Override - public void lineTo(float x, float y) throws IOException { - // TODO Auto-generated method stub - - } + @Override + public void clip(int windingRule) throws IOException { + // TODO Auto-generated method stub - @Override - public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) throws IOException { - // TODO Auto-generated method stub - - } + } - @Override - public Point2D getCurrentPoint() throws IOException { - // TODO Auto-generated method stub - return null; - } + @Override + public void moveTo(float x, float y) throws IOException { + // TODO Auto-generated method stub - @Override - public void closePath() throws IOException { - // TODO Auto-generated method stub - - } + } - @Override - public void endPath() throws IOException { - // TODO Auto-generated method stub - - } + @Override + public void lineTo(float x, float y) throws IOException { + // TODO Auto-generated method stub - @Override - public void strokePath() throws IOException { - // TODO Auto-generated method stub - - } + } - @Override - public void fillPath(int windingRule) throws IOException { - // TODO Auto-generated method stub - - } + @Override + public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) + throws IOException { + // TODO Auto-generated method stub - @Override - public void fillAndStrokePath(int windingRule) throws IOException { - // TODO Auto-generated method stub - - } + } - @Override - public void shadingFill(COSName shadingName) throws IOException { - // TODO Auto-generated method stub - - } + @Override + public Point2D getCurrentPoint() throws IOException { + // TODO Auto-generated method stub + return null; + } + + @Override + public void closePath() throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void endPath() throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void strokePath() throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void fillPath(int windingRule) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void fillAndStrokePath(int windingRule) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void shadingFill(COSName shadingName) throws IOException { + // TODO Auto-generated method stub + + } // ... rest of the overridden methods } diff --git a/src/main/java/stirling/software/SPDF/pdf/TextFinder.java b/src/main/java/stirling/software/SPDF/pdf/TextFinder.java index f7eb9e3f..cdfb5501 100644 --- a/src/main/java/stirling/software/SPDF/pdf/TextFinder.java +++ b/src/main/java/stirling/software/SPDF/pdf/TextFinder.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.pdf; + import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -13,78 +14,80 @@ import stirling.software.SPDF.model.PDFText; public class TextFinder extends PDFTextStripper { - private final String searchText; - private final boolean useRegex; - private final boolean wholeWordSearch; - private final List textOccurrences = new ArrayList<>(); + private final String searchText; + private final boolean useRegex; + private final boolean wholeWordSearch; + private final List textOccurrences = new ArrayList<>(); - public TextFinder(String searchText, boolean useRegex, boolean wholeWordSearch) throws IOException { - this.searchText = searchText.toLowerCase(); - this.useRegex = useRegex; - this.wholeWordSearch = wholeWordSearch; - setSortByPosition(true); - } + public TextFinder(String searchText, boolean useRegex, boolean wholeWordSearch) + throws IOException { + this.searchText = searchText.toLowerCase(); + this.useRegex = useRegex; + this.wholeWordSearch = wholeWordSearch; + setSortByPosition(true); + } - private List findOccurrencesInText(String searchText, String content) { - List indexes = new ArrayList<>(); - Pattern pattern; + private List findOccurrencesInText(String searchText, String content) { + List indexes = new ArrayList<>(); + Pattern pattern; - if (useRegex) { - // Use regex-based search - pattern = wholeWordSearch - ? Pattern.compile("(\\b|_|\\.)" + searchText + "(\\b|_|\\.)") - : Pattern.compile(searchText); - } else { - // Use normal text search - pattern = wholeWordSearch - ? Pattern.compile("(\\b|_|\\.)" + Pattern.quote(searchText) + "(\\b|_|\\.)") - : Pattern.compile(Pattern.quote(searchText)); - } + if (useRegex) { + // Use regex-based search + pattern = + wholeWordSearch + ? Pattern.compile("(\\b|_|\\.)" + searchText + "(\\b|_|\\.)") + : Pattern.compile(searchText); + } else { + // Use normal text search + pattern = + wholeWordSearch + ? Pattern.compile( + "(\\b|_|\\.)" + Pattern.quote(searchText) + "(\\b|_|\\.)") + : Pattern.compile(Pattern.quote(searchText)); + } - Matcher matcher = pattern.matcher(content); - while (matcher.find()) { - indexes.add(matcher.start()); - } - return indexes; - } - - @Override - protected void writeString(String text, List textPositions) { - for (Integer index : findOccurrencesInText(searchText, text.toLowerCase())) { - if (index + searchText.length() <= textPositions.size()) { - // Initial values based on the first character - TextPosition first = textPositions.get(index); - float minX = first.getX(); - float minY = first.getY(); - float maxX = first.getX() + first.getWidth(); - float maxY = first.getY() + first.getHeight(); + Matcher matcher = pattern.matcher(content); + while (matcher.find()) { + indexes.add(matcher.start()); + } + return indexes; + } - // Loop over the rest of the characters and adjust bounding box values - for (int i = index; i < index + searchText.length(); i++) { - TextPosition position = textPositions.get(i); - minX = Math.min(minX, position.getX()); - minY = Math.min(minY, position.getY()); - maxX = Math.max(maxX, position.getX() + position.getWidth()); - maxY = Math.max(maxY, position.getY() + position.getHeight()); - } + @Override + protected void writeString(String text, List textPositions) { + for (Integer index : findOccurrencesInText(searchText, text.toLowerCase())) { + if (index + searchText.length() <= textPositions.size()) { + // Initial values based on the first character + TextPosition first = textPositions.get(index); + float minX = first.getX(); + float minY = first.getY(); + float maxX = first.getX() + first.getWidth(); + float maxY = first.getY() + first.getHeight(); - textOccurrences.add(new PDFText( - getCurrentPageNo() - 1, - minX, - minY, - maxX, - maxY, - text - )); - } - } - } + // Loop over the rest of the characters and adjust bounding box values + for (int i = index; i < index + searchText.length(); i++) { + TextPosition position = textPositions.get(i); + minX = Math.min(minX, position.getX()); + minY = Math.min(minY, position.getY()); + maxX = Math.max(maxX, position.getX() + position.getWidth()); + maxY = Math.max(maxY, position.getY() + position.getHeight()); + } - public List getTextLocations(PDDocument document) throws Exception { - this.getText(document); - System.out.println("Found " + textOccurrences.size() + " occurrences of '" + searchText + "' in the document."); + textOccurrences.add( + new PDFText(getCurrentPageNo() - 1, minX, minY, maxX, maxY, text)); + } + } + } - return textOccurrences; - } + public List getTextLocations(PDDocument document) throws Exception { + this.getText(document); + System.out.println( + "Found " + + textOccurrences.size() + + " occurrences of '" + + searchText + + "' in the document."); -} \ No newline at end of file + return textOccurrences; + } +} diff --git a/src/main/java/stirling/software/SPDF/repository/AuthorityRepository.java b/src/main/java/stirling/software/SPDF/repository/AuthorityRepository.java index 62f546b8..bbf32a07 100644 --- a/src/main/java/stirling/software/SPDF/repository/AuthorityRepository.java +++ b/src/main/java/stirling/software/SPDF/repository/AuthorityRepository.java @@ -6,7 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import stirling.software.SPDF.model.Authority; -public interface AuthorityRepository extends JpaRepository { - //Set findByUsername(String username); +public interface AuthorityRepository extends JpaRepository { + // Set findByUsername(String username); Set findByUser_Username(String username); } diff --git a/src/main/java/stirling/software/SPDF/repository/JPATokenRepositoryImpl.java b/src/main/java/stirling/software/SPDF/repository/JPATokenRepositoryImpl.java index e7753903..a901d3c3 100644 --- a/src/main/java/stirling/software/SPDF/repository/JPATokenRepositoryImpl.java +++ b/src/main/java/stirling/software/SPDF/repository/JPATokenRepositoryImpl.java @@ -10,8 +10,7 @@ import stirling.software.SPDF.model.PersistentLogin; public class JPATokenRepositoryImpl implements PersistentTokenRepository { - @Autowired - private PersistentLoginRepository persistentLoginRepository; + @Autowired private PersistentLoginRepository persistentLoginRepository; @Override public void createNewToken(PersistentRememberMeToken token) { @@ -37,7 +36,8 @@ public class JPATokenRepositoryImpl implements PersistentTokenRepository { public PersistentRememberMeToken getTokenForSeries(String seriesId) { PersistentLogin token = persistentLoginRepository.findById(seriesId).orElse(null); if (token != null) { - return new PersistentRememberMeToken(token.getUsername(), token.getSeries(), token.getToken(), token.getLastUsed()); + return new PersistentRememberMeToken( + token.getUsername(), token.getSeries(), token.getToken(), token.getLastUsed()); } return null; } diff --git a/src/main/java/stirling/software/SPDF/repository/PersistentLoginRepository.java b/src/main/java/stirling/software/SPDF/repository/PersistentLoginRepository.java index 10c1acae..31841a57 100644 --- a/src/main/java/stirling/software/SPDF/repository/PersistentLoginRepository.java +++ b/src/main/java/stirling/software/SPDF/repository/PersistentLoginRepository.java @@ -4,5 +4,4 @@ import org.springframework.data.jpa.repository.JpaRepository; import stirling.software.SPDF.model.PersistentLogin; -public interface PersistentLoginRepository extends JpaRepository { -} +public interface PersistentLoginRepository extends JpaRepository {} diff --git a/src/main/java/stirling/software/SPDF/repository/UserRepository.java b/src/main/java/stirling/software/SPDF/repository/UserRepository.java index 744953d7..d63c4cba 100644 --- a/src/main/java/stirling/software/SPDF/repository/UserRepository.java +++ b/src/main/java/stirling/software/SPDF/repository/UserRepository.java @@ -8,6 +8,6 @@ import stirling.software.SPDF.model.User; public interface UserRepository extends JpaRepository { Optional findByUsername(String username); + User findByApiKey(String apiKey); } - diff --git a/src/main/java/stirling/software/SPDF/utils/ErrorUtils.java b/src/main/java/stirling/software/SPDF/utils/ErrorUtils.java index 493ce63e..e84e8d88 100644 --- a/src/main/java/stirling/software/SPDF/utils/ErrorUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/ErrorUtils.java @@ -28,5 +28,4 @@ public class ErrorUtils { modelAndView.addObject("stackTrace", stackTrace); return modelAndView; } - } diff --git a/src/main/java/stirling/software/SPDF/utils/FileToPdf.java b/src/main/java/stirling/software/SPDF/utils/FileToPdf.java index 9515a3ac..5e6825dd 100644 --- a/src/main/java/stirling/software/SPDF/utils/FileToPdf.java +++ b/src/main/java/stirling/software/SPDF/utils/FileToPdf.java @@ -14,82 +14,87 @@ import java.util.zip.ZipInputStream; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; public class FileToPdf { - public static byte[] convertHtmlToPdf(byte[] fileBytes, String fileName) throws IOException, InterruptedException { - - Path tempOutputFile = Files.createTempFile("output_", ".pdf"); - Path tempInputFile = null; - byte[] pdfBytes; - try { - if (fileName.endsWith(".html")) { - tempInputFile = Files.createTempFile("input_", ".html"); - Files.write(tempInputFile, fileBytes); - } else { - tempInputFile = unzipAndGetMainHtml(fileBytes); - } - - List command = new ArrayList<>(); - command.add("weasyprint"); - command.add(tempInputFile.toString()); - command.add(tempOutputFile.toString()); - ProcessExecutorResult returnCode; - if (fileName.endsWith(".zip")) { - returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT) - .runCommandWithOutputHandling(command, tempInputFile.getParent().toFile()); - } else { - - returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT) - .runCommandWithOutputHandling(command); - } - - pdfBytes = Files.readAllBytes(tempOutputFile); - } finally { - // Clean up temporary files - Files.delete(tempOutputFile); - Files.delete(tempInputFile); - - if (fileName.endsWith(".zip")) { - GeneralUtils.deleteDirectory(tempInputFile.getParent()); - } - } - - return pdfBytes; - } - + public static byte[] convertHtmlToPdf(byte[] fileBytes, String fileName) + throws IOException, InterruptedException { - private static Path unzipAndGetMainHtml(byte[] fileBytes) throws IOException { - Path tempDirectory = Files.createTempDirectory("unzipped_"); - try (ZipInputStream zipIn = new ZipInputStream(new ByteArrayInputStream(fileBytes))) { - ZipEntry entry = zipIn.getNextEntry(); - while (entry != null) { - Path filePath = tempDirectory.resolve(entry.getName()); - if (entry.isDirectory()) { - Files.createDirectories(filePath); // Explicitly create the directory structure - } else { - Files.createDirectories(filePath.getParent()); // Create parent directories if they don't exist - Files.copy(zipIn, filePath); - } - zipIn.closeEntry(); - entry = zipIn.getNextEntry(); - } - } + Path tempOutputFile = Files.createTempFile("output_", ".pdf"); + Path tempInputFile = null; + byte[] pdfBytes; + try { + if (fileName.endsWith(".html")) { + tempInputFile = Files.createTempFile("input_", ".html"); + Files.write(tempInputFile, fileBytes); + } else { + tempInputFile = unzipAndGetMainHtml(fileBytes); + } - //search for the main HTML file. - try (Stream walk = Files.walk(tempDirectory)) { - List htmlFiles = walk.filter(file -> file.toString().endsWith(".html")) - .collect(Collectors.toList()); + List command = new ArrayList<>(); + command.add("weasyprint"); + command.add(tempInputFile.toString()); + command.add(tempOutputFile.toString()); + ProcessExecutorResult returnCode; + if (fileName.endsWith(".zip")) { + returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT) + .runCommandWithOutputHandling( + command, tempInputFile.getParent().toFile()); + } else { - if (htmlFiles.isEmpty()) { - throw new IOException("No HTML files found in the unzipped directory."); - } + returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT) + .runCommandWithOutputHandling(command); + } - // Prioritize 'index.html' if it exists, otherwise use the first .html file - for (Path htmlFile : htmlFiles) { - if (htmlFile.getFileName().toString().equals("index.html")) { - return htmlFile; - } - } + pdfBytes = Files.readAllBytes(tempOutputFile); + } finally { + // Clean up temporary files + Files.delete(tempOutputFile); + Files.delete(tempInputFile); - return htmlFiles.get(0); - } - } + if (fileName.endsWith(".zip")) { + GeneralUtils.deleteDirectory(tempInputFile.getParent()); + } + } + + return pdfBytes; + } + + private static Path unzipAndGetMainHtml(byte[] fileBytes) throws IOException { + Path tempDirectory = Files.createTempDirectory("unzipped_"); + try (ZipInputStream zipIn = new ZipInputStream(new ByteArrayInputStream(fileBytes))) { + ZipEntry entry = zipIn.getNextEntry(); + while (entry != null) { + Path filePath = tempDirectory.resolve(entry.getName()); + if (entry.isDirectory()) { + Files.createDirectories(filePath); // Explicitly create the directory structure + } else { + Files.createDirectories( + filePath.getParent()); // Create parent directories if they don't exist + Files.copy(zipIn, filePath); + } + zipIn.closeEntry(); + entry = zipIn.getNextEntry(); + } + } + + // search for the main HTML file. + try (Stream walk = Files.walk(tempDirectory)) { + List htmlFiles = + walk.filter(file -> file.toString().endsWith(".html")) + .collect(Collectors.toList()); + + if (htmlFiles.isEmpty()) { + throw new IOException("No HTML files found in the unzipped directory."); + } + + // Prioritize 'index.html' if it exists, otherwise use the first .html file + for (Path htmlFile : htmlFiles) { + if (htmlFile.getFileName().toString().equals("index.html")) { + return htmlFile; + } + } + + return htmlFiles.get(0); + } + } } diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java index 6de7df14..a1e177e4 100644 --- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java @@ -16,46 +16,50 @@ import java.util.ArrayList; import java.util.List; import org.springframework.web.multipart.MultipartFile; + public class GeneralUtils { - public static void deleteDirectory(Path path) throws IOException { - Files.walkFileTree(path, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } + public static void deleteDirectory(Path path) throws IOException { + Files.walkFileTree( + path, + new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } - }); + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) + throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); } - public static String convertToFileName(String name) { + public static String convertToFileName(String name) { String safeName = name.replaceAll("[^a-zA-Z0-9]", "_"); if (safeName.length() > 50) { safeName = safeName.substring(0, 50); } return safeName; } - - - public static boolean isValidURL(String urlStr) { - try { - new URL(urlStr); - return true; - } catch (MalformedURLException e) { - return false; - } - } - public static File multipartToFile(MultipartFile multipart) throws IOException { + public static boolean isValidURL(String urlStr) { + try { + new URL(urlStr); + return true; + } catch (MalformedURLException e) { + return false; + } + } + + public static File multipartToFile(MultipartFile multipart) throws IOException { Path tempFile = Files.createTempFile("overlay-", ".pdf"); - try (InputStream in = multipart.getInputStream(); - FileOutputStream out = new FileOutputStream(tempFile.toFile())) { + try (InputStream in = multipart.getInputStream(); + FileOutputStream out = new FileOutputStream(tempFile.toFile())) { byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = in.read(buffer)) != -1) { @@ -64,110 +68,119 @@ public class GeneralUtils { } return tempFile.toFile(); } - - public static Long convertSizeToBytes(String sizeStr) { - if (sizeStr == null) { - return null; - } - - sizeStr = sizeStr.trim().toUpperCase(); - try { - if (sizeStr.endsWith("KB")) { - return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024); - } else if (sizeStr.endsWith("MB")) { - return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024); - } else if (sizeStr.endsWith("GB")) { - return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024 * 1024); - } else if (sizeStr.endsWith("B")) { - return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1)); - } else { - // Assume MB if no unit is specified - return (long) (Double.parseDouble(sizeStr) * 1024 * 1024); - } - } catch (NumberFormatException e) { - // The numeric part of the input string cannot be parsed, handle this case - } - - return null; - } - public static List parsePageString(String pageOrder, int totalPages) { - return parsePageList(pageOrder.split(","), totalPages); - } - public static List parsePageList(String[] pageOrderArr, int totalPages) { - List newPageOrder = new ArrayList<>(); + public static Long convertSizeToBytes(String sizeStr) { + if (sizeStr == null) { + return null; + } - // loop through the page order array - for (String element : pageOrderArr) { - if (element.equalsIgnoreCase("all")) { - for (int i = 0; i < totalPages; i++) { - newPageOrder.add(i); - } - // As all pages are already added, no need to check further - break; - } - else if (element.matches("\\d*n\\+?-?\\d*|\\d*\\+?n")) { - // Handle page order as a function - int coefficient = 0; - int constant = 0; - boolean coefficientExists = false; - boolean constantExists = false; + sizeStr = sizeStr.trim().toUpperCase(); + try { + if (sizeStr.endsWith("KB")) { + return (long) + (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024); + } else if (sizeStr.endsWith("MB")) { + return (long) + (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) + * 1024 + * 1024); + } else if (sizeStr.endsWith("GB")) { + return (long) + (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) + * 1024 + * 1024 + * 1024); + } else if (sizeStr.endsWith("B")) { + return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1)); + } else { + // Assume MB if no unit is specified + return (long) (Double.parseDouble(sizeStr) * 1024 * 1024); + } + } catch (NumberFormatException e) { + // The numeric part of the input string cannot be parsed, handle this case + } - if (element.contains("n")) { - String[] parts = element.split("n"); - if (!parts[0].equals("") && parts[0] != null) { - coefficient = Integer.parseInt(parts[0]); - coefficientExists = true; - } - if (parts.length > 1 && !parts[1].equals("") && parts[1] != null) { - constant = Integer.parseInt(parts[1]); - constantExists = true; - } - } else if (element.contains("+")) { - constant = Integer.parseInt(element.replace("+", "")); - constantExists = true; - } + return null; + } - for (int i = 1; i <= totalPages; i++) { - int pageNum = coefficientExists ? coefficient * i : i; - pageNum += constantExists ? constant : 0; + public static List parsePageString(String pageOrder, int totalPages) { + return parsePageList(pageOrder.split(","), totalPages); + } - if (pageNum <= totalPages && pageNum > 0) { - newPageOrder.add(pageNum - 1); - } - } - } else if (element.contains("-")) { - // split the range into start and end page - String[] range = element.split("-"); - int start = Integer.parseInt(range[0]); - int end = Integer.parseInt(range[1]); - // check if the end page is greater than total pages - if (end > totalPages) { - end = totalPages; - } - // loop through the range of pages - for (int j = start; j <= end; j++) { - // print the current index - newPageOrder.add(j - 1); - } - } else { - // if the element is a single page - newPageOrder.add(Integer.parseInt(element) - 1); - } - } + public static List parsePageList(String[] pageOrderArr, int totalPages) { + List newPageOrder = new ArrayList<>(); - return newPageOrder; - } - public static boolean createDir(String path) { - Path folder = Paths.get(path); - if (!Files.exists(folder)) { - try { - Files.createDirectories(folder); - } catch (IOException e) { - e.printStackTrace(); - return false; - } - } - return true; - } + // loop through the page order array + for (String element : pageOrderArr) { + if (element.equalsIgnoreCase("all")) { + for (int i = 0; i < totalPages; i++) { + newPageOrder.add(i); + } + // As all pages are already added, no need to check further + break; + } else if (element.matches("\\d*n\\+?-?\\d*|\\d*\\+?n")) { + // Handle page order as a function + int coefficient = 0; + int constant = 0; + boolean coefficientExists = false; + boolean constantExists = false; + + if (element.contains("n")) { + String[] parts = element.split("n"); + if (!parts[0].equals("") && parts[0] != null) { + coefficient = Integer.parseInt(parts[0]); + coefficientExists = true; + } + if (parts.length > 1 && !parts[1].equals("") && parts[1] != null) { + constant = Integer.parseInt(parts[1]); + constantExists = true; + } + } else if (element.contains("+")) { + constant = Integer.parseInt(element.replace("+", "")); + constantExists = true; + } + + for (int i = 1; i <= totalPages; i++) { + int pageNum = coefficientExists ? coefficient * i : i; + pageNum += constantExists ? constant : 0; + + if (pageNum <= totalPages && pageNum > 0) { + newPageOrder.add(pageNum - 1); + } + } + } else if (element.contains("-")) { + // split the range into start and end page + String[] range = element.split("-"); + int start = Integer.parseInt(range[0]); + int end = Integer.parseInt(range[1]); + // check if the end page is greater than total pages + if (end > totalPages) { + end = totalPages; + } + // loop through the range of pages + for (int j = start; j <= end; j++) { + // print the current index + newPageOrder.add(j - 1); + } + } else { + // if the element is a single page + newPageOrder.add(Integer.parseInt(element) - 1); + } + } + + return newPageOrder; + } + + public static boolean createDir(String path) { + Path folder = Paths.get(path); + if (!Files.exists(folder)) { + try { + Files.createDirectories(folder); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + return true; + } } diff --git a/src/main/java/stirling/software/SPDF/utils/ImageProcessingUtils.java b/src/main/java/stirling/software/SPDF/utils/ImageProcessingUtils.java index e34892d5..ede9c4f4 100644 --- a/src/main/java/stirling/software/SPDF/utils/ImageProcessingUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/ImageProcessingUtils.java @@ -4,22 +4,29 @@ import java.awt.image.BufferedImage; public class ImageProcessingUtils { - static BufferedImage convertColorType(BufferedImage sourceImage, String colorType) { - BufferedImage convertedImage; - switch (colorType) { - case "greyscale": - convertedImage = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), BufferedImage.TYPE_BYTE_GRAY); - convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null); - break; - case "blackwhite": - convertedImage = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), BufferedImage.TYPE_BYTE_BINARY); - convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null); - break; - default: // full color - convertedImage = sourceImage; - break; - } - return convertedImage; - } - + static BufferedImage convertColorType(BufferedImage sourceImage, String colorType) { + BufferedImage convertedImage; + switch (colorType) { + case "greyscale": + convertedImage = + new BufferedImage( + sourceImage.getWidth(), + sourceImage.getHeight(), + BufferedImage.TYPE_BYTE_GRAY); + convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null); + break; + case "blackwhite": + convertedImage = + new BufferedImage( + sourceImage.getWidth(), + sourceImage.getHeight(), + BufferedImage.TYPE_BYTE_BINARY); + convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null); + break; + default: // full color + convertedImage = sourceImage; + break; + } + return convertedImage; + } } diff --git a/src/main/java/stirling/software/SPDF/utils/PDFManipulationUtils.java b/src/main/java/stirling/software/SPDF/utils/PDFManipulationUtils.java index 5718cd8a..397ff5bc 100644 --- a/src/main/java/stirling/software/SPDF/utils/PDFManipulationUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/PDFManipulationUtils.java @@ -1,5 +1,3 @@ package stirling.software.SPDF.utils; -public class PDFManipulationUtils { - -} +public class PDFManipulationUtils {} diff --git a/src/main/java/stirling/software/SPDF/utils/PDFToFile.java b/src/main/java/stirling/software/SPDF/utils/PDFToFile.java index af658f79..a7f751af 100644 --- a/src/main/java/stirling/software/SPDF/utils/PDFToFile.java +++ b/src/main/java/stirling/software/SPDF/utils/PDFToFile.java @@ -23,7 +23,9 @@ import org.springframework.web.multipart.MultipartFile; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; public class PDFToFile { - public ResponseEntity processPdfToOfficeFormat(MultipartFile inputFile, String outputFormat, String libreOfficeFilter) throws IOException, InterruptedException { + public ResponseEntity processPdfToOfficeFormat( + MultipartFile inputFile, String outputFormat, String libreOfficeFilter) + throws IOException, InterruptedException { if (!"application/pdf".equals(inputFile.getContentType())) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); @@ -34,7 +36,18 @@ public class PDFToFile { String pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.')); // Validate output format - List allowedFormats = Arrays.asList("doc", "docx", "odt", "ppt", "pptx", "odp", "rtf", "html", "xml", "txt:Text"); + List allowedFormats = + Arrays.asList( + "doc", + "docx", + "odt", + "ppt", + "pptx", + "odp", + "rtf", + "html", + "xml", + "txt:Text"); if (!allowedFormats.contains(outputFormat)) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } @@ -47,15 +60,26 @@ public class PDFToFile { try { // Save the uploaded file to a temporary location tempInputFile = Files.createTempFile("input_", ".pdf"); - Files.copy(inputFile.getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING); + Files.copy( + inputFile.getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING); // Prepare the output directory tempOutputDir = Files.createTempDirectory("output_"); // Run the LibreOffice command - List command = new ArrayList<>( - Arrays.asList("soffice", "--infilter=" + libreOfficeFilter, "--convert-to", outputFormat, "--outdir", tempOutputDir.toString(), tempInputFile.toString())); - ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE).runCommandWithOutputHandling(command); + List command = + new ArrayList<>( + Arrays.asList( + "soffice", + "--infilter=" + libreOfficeFilter, + "--convert-to", + outputFormat, + "--outdir", + tempOutputDir.toString(), + tempInputFile.toString())); + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE) + .runCommandWithOutputHandling(command); // Get output files List outputFiles = Arrays.asList(tempOutputDir.toFile().listFiles()); @@ -89,11 +113,10 @@ public class PDFToFile { } finally { // Clean up the temporary files - if (tempInputFile != null) - Files.delete(tempInputFile); - if (tempOutputDir != null) - FileUtils.deleteDirectory(tempOutputDir.toFile()); + if (tempInputFile != null) Files.delete(tempInputFile); + if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile()); } - return WebResponseUtils.bytesToWebResponse(fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.bytesToWebResponse( + fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM); } } diff --git a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java index 038b8302..677bafd1 100644 --- a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java @@ -5,7 +5,6 @@ import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.util.ArrayList; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -37,38 +36,35 @@ public class PdfUtils { private static final Logger logger = LoggerFactory.getLogger(PdfUtils.class); + public static PDRectangle textToPageSize(String size) { + switch (size.toUpperCase()) { + case "A0": + return PDRectangle.A0; + case "A1": + return PDRectangle.A1; + case "A2": + return PDRectangle.A2; + case "A3": + return PDRectangle.A3; + case "A4": + return PDRectangle.A4; + case "A5": + return PDRectangle.A5; + case "A6": + return PDRectangle.A6; + case "LETTER": + return PDRectangle.LETTER; + case "LEGAL": + return PDRectangle.LEGAL; + default: + throw new IllegalArgumentException("Invalid standard page size: " + size); + } + } - public static PDRectangle textToPageSize(String size) { - switch (size.toUpperCase()) { - case "A0": - return PDRectangle.A0; - case "A1": - return PDRectangle.A1; - case "A2": - return PDRectangle.A2; - case "A3": - return PDRectangle.A3; - case "A4": - return PDRectangle.A4; - case "A5": - return PDRectangle.A5; - case "A6": - return PDRectangle.A6; - case "LETTER": - return PDRectangle.LETTER; - case "LEGAL": - return PDRectangle.LEGAL; - default: - throw new IllegalArgumentException("Invalid standard page size: " + size); - } - } - - - - public static boolean hasImages(PDDocument document, String pagesToCheck) throws IOException { String[] pageOrderArr = pagesToCheck.split(","); - List pageList = GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); + List pageList = + GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); for (int pageNumber : pageList) { PDPage page = document.getPage(pageNumber); @@ -80,9 +76,11 @@ public class PdfUtils { return false; } - public static boolean hasText(PDDocument document, String pageNumbersToCheck, String phrase) throws IOException { + public static boolean hasText(PDDocument document, String pageNumbersToCheck, String phrase) + throws IOException { String[] pageOrderArr = pageNumbersToCheck.split(","); - List pageList = GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); + List pageList = + GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); for (int pageNumber : pageList) { PDPage page = document.getPage(pageNumber); @@ -94,15 +92,11 @@ public class PdfUtils { return false; } - public static boolean hasImagesOnPage(PDPage page) throws IOException { ImageFinder imageFinder = new ImageFinder(page); imageFinder.processPage(page); return imageFinder.hasImages(); } - - - public static boolean hasTextOnPage(PDPage page, String phrase) throws IOException { PDFTextStripper textStripper = new PDFTextStripper(); @@ -113,12 +107,12 @@ public class PdfUtils { return pageText.contains(phrase); } - - public boolean containsTextInFile(PDDocument pdfDocument, String text, String pagesToCheck) throws IOException { + public boolean containsTextInFile(PDDocument pdfDocument, String text, String pagesToCheck) + throws IOException { PDFTextStripper textStripper = new PDFTextStripper(); String pdfText = ""; - if(pagesToCheck == null || pagesToCheck.equals("all")) { + if (pagesToCheck == null || pagesToCheck.equals("all")) { pdfText = textStripper.getText(pdfDocument); } else { // remove whitespaces @@ -152,15 +146,12 @@ public class PdfUtils { return pdfText.contains(text); } - - - - - public boolean pageCount(PDDocument pdfDocument, int pageCount, String comparator) throws IOException { + public boolean pageCount(PDDocument pdfDocument, int pageCount, String comparator) + throws IOException { int actualPageCount = pdfDocument.getNumberOfPages(); pdfDocument.close(); - switch(comparator.toLowerCase()) { + switch (comparator.toLowerCase()) { case "greater": return actualPageCount > pageCount; case "equal": @@ -168,7 +159,8 @@ public class PdfUtils { case "less": return actualPageCount < pageCount; default: - throw new IllegalArgumentException("Invalid comparator. Only 'greater', 'equal', and 'less' are supported."); + throw new IllegalArgumentException( + "Invalid comparator. Only 'greater', 'equal', and 'less' are supported."); } } @@ -189,12 +181,15 @@ public class PdfUtils { // Checks if the actual page size matches the expected page size return actualPageWidth == expectedPageWidth && actualPageHeight == expectedPageHeight; } - - - - - - public static byte[] convertFromPdf(byte[] inputStream, String imageType, ImageType colorType, boolean singleImage, int DPI, String filename) throws IOException, Exception { + + public static byte[] convertFromPdf( + byte[] inputStream, + String imageType, + ImageType colorType, + boolean singleImage, + int DPI, + String filename) + throws IOException, Exception { try (PDDocument document = PDDocument.load(new ByteArrayInputStream(inputStream))) { PDFRenderer pdfRenderer = new PDFRenderer(document); int pageCount = document.getNumberOfPages(); @@ -203,7 +198,8 @@ public class PdfUtils { ByteArrayOutputStream baos = new ByteArrayOutputStream(); if (singleImage) { - if (imageType.toLowerCase().equals("tiff") || imageType.toLowerCase().equals("tif")) { + if (imageType.toLowerCase().equals("tiff") + || imageType.toLowerCase().equals("tif")) { // Write the images to the output stream as a TIFF with multiple frames ImageWriter writer = ImageIO.getImageWritersByFormatName("tiff").next(); ImageWriteParam param = writer.getDefaultWriteParam(); @@ -227,13 +223,17 @@ public class PdfUtils { } else { // Combine all images into a single big image BufferedImage image = pdfRenderer.renderImageWithDPI(0, DPI, colorType); - BufferedImage combined = new BufferedImage(image.getWidth(), image.getHeight() * pageCount, BufferedImage.TYPE_INT_RGB); + BufferedImage combined = + new BufferedImage( + image.getWidth(), + image.getHeight() * pageCount, + BufferedImage.TYPE_INT_RGB); Graphics g = combined.getGraphics(); for (int i = 0; i < pageCount; ++i) { if (i != 0) { image = pdfRenderer.renderImageWithDPI(i, DPI, colorType); - } + } g.drawImage(image, 0, i * image.getHeight(), null); } @@ -252,7 +252,12 @@ public class PdfUtils { ImageIO.write(image, imageType, baosImage); // Add the image to the zip file - zos.putNextEntry(new ZipEntry(String.format(filename + "_%d.%s", i + 1, imageType.toLowerCase()))); + zos.putNextEntry( + new ZipEntry( + String.format( + filename + "_%d.%s", + i + 1, + imageType.toLowerCase()))); zos.write(baosImage.toByteArray()); } } @@ -267,28 +272,37 @@ public class PdfUtils { throw e; } } - public static byte[] imageToPdf(MultipartFile[] files, String fitOption, boolean autoRotate, String colorType) throws IOException { + + public static byte[] imageToPdf( + MultipartFile[] files, String fitOption, boolean autoRotate, String colorType) + throws IOException { try (PDDocument doc = new PDDocument()) { for (MultipartFile file : files) { - String contentType = file.getContentType(); + String contentType = file.getContentType(); String originalFilename = file.getOriginalFilename(); - if (originalFilename != null && (originalFilename.toLowerCase().endsWith(".tiff") || originalFilename.toLowerCase().endsWith(".tif")) ) { + if (originalFilename != null + && (originalFilename.toLowerCase().endsWith(".tiff") + || originalFilename.toLowerCase().endsWith(".tif"))) { ImageReader reader = ImageIO.getImageReadersByFormatName("tiff").next(); reader.setInput(ImageIO.createImageInputStream(file.getInputStream())); int numPages = reader.getNumImages(true); for (int i = 0; i < numPages; i++) { BufferedImage pageImage = reader.read(i); - BufferedImage convertedImage = ImageProcessingUtils.convertColorType(pageImage, colorType); - PDImageXObject pdImage = LosslessFactory.createFromImage(doc, convertedImage); + BufferedImage convertedImage = + ImageProcessingUtils.convertColorType(pageImage, colorType); + PDImageXObject pdImage = + LosslessFactory.createFromImage(doc, convertedImage); addImageToDocument(doc, pdImage, fitOption, autoRotate); } } else { BufferedImage image = ImageIO.read(file.getInputStream()); - BufferedImage convertedImage = ImageProcessingUtils.convertColorType(image, colorType); + BufferedImage convertedImage = + ImageProcessingUtils.convertColorType(image, colorType); // Use JPEGFactory if it's JPEG since JPEG is lossy - PDImageXObject pdImage = (contentType != null && contentType.equals("image/jpeg")) - ? JPEGFactory.createFromImage(doc, convertedImage) - : LosslessFactory.createFromImage(doc, convertedImage); + PDImageXObject pdImage = + (contentType != null && contentType.equals("image/jpeg")) + ? JPEGFactory.createFromImage(doc, convertedImage) + : LosslessFactory.createFromImage(doc, convertedImage); addImageToDocument(doc, pdImage, fitOption, autoRotate); } } @@ -299,11 +313,13 @@ public class PdfUtils { } } - private static void addImageToDocument(PDDocument doc, PDImageXObject image, String fitOption, boolean autoRotate) throws IOException { + private static void addImageToDocument( + PDDocument doc, PDImageXObject image, String fitOption, boolean autoRotate) + throws IOException { boolean imageIsLandscape = image.getWidth() > image.getHeight(); PDRectangle pageSize = PDRectangle.A4; - System.out.println(fitOption); + System.out.println(fitOption); if (autoRotate && imageIsLandscape) { pageSize = new PDRectangle(pageSize.getHeight(), pageSize.getWidth()); @@ -335,7 +351,12 @@ public class PdfUtils { float xPos = (pageWidth - (image.getWidth() * scaleFactor)) / 2; float yPos = (pageHeight - (image.getHeight() * scaleFactor)) / 2; - contentStream.drawImage(image, xPos, yPos, image.getWidth() * scaleFactor, image.getHeight() * scaleFactor); + contentStream.drawImage( + image, + xPos, + yPos, + image.getWidth() * scaleFactor, + image.getHeight() * scaleFactor); } } catch (IOException e) { logger.error("Error adding image to PDF", e); @@ -343,8 +364,9 @@ public class PdfUtils { } } - - public static byte[] overlayImage(byte[] pdfBytes, byte[] imageBytes, float x, float y, boolean everyPage) throws IOException { + public static byte[] overlayImage( + byte[] pdfBytes, byte[] imageBytes, float x, float y, boolean everyPage) + throws IOException { PDDocument document = PDDocument.load(new ByteArrayInputStream(pdfBytes)); @@ -352,7 +374,9 @@ public class PdfUtils { int pages = document.getNumberOfPages(); for (int i = 0; i < pages; i++) { PDPage page = document.getPage(i); - try (PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true)) { + try (PDPageContentStream contentStream = + new PDPageContentStream( + document, page, PDPageContentStream.AppendMode.APPEND, true)) { // Create an image object from the image bytes PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, ""); // Draw the image onto the page at the specified x and y coordinates @@ -366,7 +390,6 @@ public class PdfUtils { logger.error("Error overlaying image onto PDF", e); throw e; } - } // Create a ByteArrayOutputStream to save the PDF to ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -374,8 +397,4 @@ public class PdfUtils { logger.info("PDF successfully saved to byte array"); return baos.toByteArray(); } - - - - } diff --git a/src/main/java/stirling/software/SPDF/utils/ProcessExecutor.java b/src/main/java/stirling/software/SPDF/utils/ProcessExecutor.java index fe5c6717..385f3b80 100644 --- a/src/main/java/stirling/software/SPDF/utils/ProcessExecutor.java +++ b/src/main/java/stirling/software/SPDF/utils/ProcessExecutor.java @@ -14,22 +14,29 @@ import java.util.concurrent.Semaphore; public class ProcessExecutor { public enum Processes { - LIBRE_OFFICE, OCR_MY_PDF, PYTHON_OPENCV, GHOSTSCRIPT, WEASYPRINT + LIBRE_OFFICE, + OCR_MY_PDF, + PYTHON_OPENCV, + GHOSTSCRIPT, + WEASYPRINT } private static final Map instances = new ConcurrentHashMap<>(); public static ProcessExecutor getInstance(Processes processType) { - return instances.computeIfAbsent(processType, key -> { - int semaphoreLimit = switch (key) { - case LIBRE_OFFICE -> 1; - case OCR_MY_PDF -> 2; - case PYTHON_OPENCV -> 8; - case GHOSTSCRIPT -> 16; - case WEASYPRINT -> 16; - }; - return new ProcessExecutor(semaphoreLimit); - }); + return instances.computeIfAbsent( + processType, + key -> { + int semaphoreLimit = + switch (key) { + case LIBRE_OFFICE -> 1; + case OCR_MY_PDF -> 2; + case PYTHON_OPENCV -> 8; + case GHOSTSCRIPT -> 16; + case WEASYPRINT -> 16; + }; + return new ProcessExecutor(semaphoreLimit); + }); } private final Semaphore semaphore; @@ -37,10 +44,14 @@ public class ProcessExecutor { private ProcessExecutor(int semaphoreLimit) { this.semaphore = new Semaphore(semaphoreLimit); } - public ProcessExecutorResult runCommandWithOutputHandling(List command) throws IOException, InterruptedException { - return runCommandWithOutputHandling(command, null); + + public ProcessExecutorResult runCommandWithOutputHandling(List command) + throws IOException, InterruptedException { + return runCommandWithOutputHandling(command, null); } - public ProcessExecutorResult runCommandWithOutputHandling(List command, File workingDirectory) throws IOException, InterruptedException { + + public ProcessExecutorResult runCommandWithOutputHandling( + List command, File workingDirectory) throws IOException, InterruptedException { int exitCode = 1; String messages = ""; semaphore.acquire(); @@ -48,7 +59,7 @@ public class ProcessExecutor { System.out.print("Running command: " + String.join(" ", command)); ProcessBuilder processBuilder = new ProcessBuilder(command); - + // Use the working directory if it's set if (workingDirectory != null) { processBuilder.directory(workingDirectory); @@ -59,27 +70,39 @@ public class ProcessExecutor { List errorLines = new ArrayList<>(); List outputLines = new ArrayList<>(); - Thread errorReaderThread = new Thread(() -> { - try (BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) { - String line; - while ((line = errorReader.readLine()) != null) { - errorLines.add(line); - } - } catch (IOException e) { - e.printStackTrace(); - } - }); + Thread errorReaderThread = + new Thread( + () -> { + try (BufferedReader errorReader = + new BufferedReader( + new InputStreamReader( + process.getErrorStream(), + StandardCharsets.UTF_8))) { + String line; + while ((line = errorReader.readLine()) != null) { + errorLines.add(line); + } + } catch (IOException e) { + e.printStackTrace(); + } + }); - Thread outputReaderThread = new Thread(() -> { - try (BufferedReader outputReader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { - String line; - while ((line = outputReader.readLine()) != null) { - outputLines.add(line); - } - } catch (IOException e) { - e.printStackTrace(); - } - }); + Thread outputReaderThread = + new Thread( + () -> { + try (BufferedReader outputReader = + new BufferedReader( + new InputStreamReader( + process.getInputStream(), + StandardCharsets.UTF_8))) { + String line; + while ((line = outputReader.readLine()) != null) { + outputLines.add(line); + } + } catch (IOException e) { + e.printStackTrace(); + } + }); errorReaderThread.start(); outputReaderThread.start(); @@ -90,7 +113,7 @@ public class ProcessExecutor { // Wait for the reader threads to finish errorReaderThread.join(); outputReaderThread.join(); - + if (outputLines.size() > 0) { String outputMessage = String.join("\n", outputLines); messages += outputMessage; @@ -102,7 +125,11 @@ public class ProcessExecutor { messages += errorMessage; System.out.println("Command error output:\n" + errorMessage); if (exitCode != 0) { - throw new IOException("Command process failed with exit code " + exitCode + ". Error message: " + errorMessage); + throw new IOException( + "Command process failed with exit code " + + exitCode + + ". Error message: " + + errorMessage); } } } finally { @@ -110,26 +137,30 @@ public class ProcessExecutor { } return new ProcessExecutorResult(exitCode, messages); } - public class ProcessExecutorResult{ - int rc; - String messages; - public ProcessExecutorResult(int rc, String messages) { - this.rc = rc; - this.messages = messages; - } - public int getRc() { - return rc; - } - public void setRc(int rc) { - this.rc = rc; - } - public String getMessages() { - return messages; - } - public void setMessages(String messages) { - this.messages = messages; - } - - + + public class ProcessExecutorResult { + int rc; + String messages; + + public ProcessExecutorResult(int rc, String messages) { + this.rc = rc; + this.messages = messages; + } + + public int getRc() { + return rc; + } + + public void setRc(int rc) { + this.rc = rc; + } + + public String getMessages() { + return messages; + } + + public void setMessages(String messages) { + this.messages = messages; + } } } diff --git a/src/main/java/stirling/software/SPDF/utils/PropertyConfigs.java b/src/main/java/stirling/software/SPDF/utils/PropertyConfigs.java index 8d12267c..aa3e453d 100644 --- a/src/main/java/stirling/software/SPDF/utils/PropertyConfigs.java +++ b/src/main/java/stirling/software/SPDF/utils/PropertyConfigs.java @@ -4,46 +4,35 @@ import java.util.List; public class PropertyConfigs { - - public static boolean getBooleanValue(List keys, boolean defaultValue) { - for (String key : keys) { - String value = System.getProperty(key); - if (value == null) - value = System.getenv(key); - - if (value != null) - return Boolean.valueOf(value); - } - return defaultValue; - } + public static boolean getBooleanValue(List keys, boolean defaultValue) { + for (String key : keys) { + String value = System.getProperty(key); + if (value == null) value = System.getenv(key); - public static String getStringValue(List keys, String defaultValue) { - for (String key : keys) { - String value = System.getProperty(key); - if (value == null) - value = System.getenv(key); - - if (value != null) - return value; - } - return defaultValue; - } + if (value != null) return Boolean.valueOf(value); + } + return defaultValue; + } - - - - public static boolean getBooleanValue(String key, boolean defaultValue) { + public static String getStringValue(List keys, String defaultValue) { + for (String key : keys) { + String value = System.getProperty(key); + if (value == null) value = System.getenv(key); + + if (value != null) return value; + } + return defaultValue; + } + + public static boolean getBooleanValue(String key, boolean defaultValue) { String value = System.getProperty(key); - if (value == null) - value = System.getenv(key); + if (value == null) value = System.getenv(key); return (value != null) ? Boolean.valueOf(value) : defaultValue; } - public static String getStringValue(String key, String defaultValue) { + public static String getStringValue(String key, String defaultValue) { String value = System.getProperty(key); - if (value == null) - value = System.getenv(key); + if (value == null) value = System.getenv(key); return (value != null) ? value : defaultValue; } - } diff --git a/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java b/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java index 0046ee9f..b320f67e 100644 --- a/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java @@ -2,15 +2,13 @@ package stirling.software.SPDF.utils; public class RequestUriUtils { - public static boolean isStaticResource(String requestURI) { - - return requestURI.startsWith("/css/") - || requestURI.startsWith("/js/") - || requestURI.startsWith("/images/") - || requestURI.startsWith("/public/") - || requestURI.startsWith("/pdfjs/") - || requestURI.endsWith(".svg"); - - } + public static boolean isStaticResource(String requestURI) { + return requestURI.startsWith("/css/") + || requestURI.startsWith("/js/") + || requestURI.startsWith("/images/") + || requestURI.startsWith("/public/") + || requestURI.startsWith("/pdfjs/") + || requestURI.endsWith(".svg"); + } } diff --git a/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java b/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java index 131aaf03..4958a00d 100644 --- a/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java @@ -12,53 +12,56 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.multipart.MultipartFile; - public class WebResponseUtils { - public static ResponseEntity boasToWebResponse(ByteArrayOutputStream baos, String docName) throws IOException { - return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName); - } + public static ResponseEntity boasToWebResponse( + ByteArrayOutputStream baos, String docName) throws IOException { + return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName); + } - public static ResponseEntity boasToWebResponse(ByteArrayOutputStream baos, String docName, MediaType mediaType) throws IOException { - return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName, mediaType); - } + public static ResponseEntity boasToWebResponse( + ByteArrayOutputStream baos, String docName, MediaType mediaType) throws IOException { + return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName, mediaType); + } + public static ResponseEntity multiPartFileToWebResponse(MultipartFile file) + throws IOException { + String fileName = file.getOriginalFilename(); + MediaType mediaType = MediaType.parseMediaType(file.getContentType()); - public static ResponseEntity multiPartFileToWebResponse(MultipartFile file) throws IOException { - String fileName = file.getOriginalFilename(); - MediaType mediaType = MediaType.parseMediaType(file.getContentType()); + byte[] bytes = file.getBytes(); - byte[] bytes = file.getBytes(); - - return bytesToWebResponse(bytes, fileName, mediaType); - } + return bytesToWebResponse(bytes, fileName, mediaType); + } - public static ResponseEntity bytesToWebResponse(byte[] bytes, String docName, MediaType mediaType) throws IOException { - - // Return the PDF as a response - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(mediaType); - headers.setContentLength(bytes.length); - String encodedDocName = URLEncoder.encode(docName, StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20"); - headers.setContentDispositionFormData("attachment", encodedDocName); - return new ResponseEntity<>(bytes, headers, HttpStatus.OK); - } + public static ResponseEntity bytesToWebResponse( + byte[] bytes, String docName, MediaType mediaType) throws IOException { - public static ResponseEntity bytesToWebResponse(byte[] bytes, String docName) throws IOException { - return bytesToWebResponse(bytes, docName, MediaType.APPLICATION_PDF); - } + // Return the PDF as a response + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(mediaType); + headers.setContentLength(bytes.length); + String encodedDocName = + URLEncoder.encode(docName, StandardCharsets.UTF_8.toString()) + .replaceAll("\\+", "%20"); + headers.setContentDispositionFormData("attachment", encodedDocName); + return new ResponseEntity<>(bytes, headers, HttpStatus.OK); + } - public static ResponseEntity pdfDocToWebResponse(PDDocument document, String docName) throws IOException { - - // Open Byte Array and save document to it - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.save(baos); - // Close the document - document.close(); - - return boasToWebResponse(baos, docName); - } - + public static ResponseEntity bytesToWebResponse(byte[] bytes, String docName) + throws IOException { + return bytesToWebResponse(bytes, docName, MediaType.APPLICATION_PDF); + } + public static ResponseEntity pdfDocToWebResponse(PDDocument document, String docName) + throws IOException { + // Open Byte Array and save document to it + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + document.save(baos); + // Close the document + document.close(); + + return boasToWebResponse(baos, docName); + } } diff --git a/src/main/resources/static/pdfjs/cmaps/CNS2-V.bcmap b/src/main/resources/static/pdfjs/cmaps/CNS2-V.bcmap index 7588cec8..9cfbf508 100644 --- a/src/main/resources/static/pdfjs/cmaps/CNS2-V.bcmap +++ b/src/main/resources/static/pdfjs/cmaps/CNS2-V.bcmap @@ -1,3 +1,3 @@ -RCopyright 1990-2009 Adobe Systems Incorporated. -All rights reserved. +RCopyright 1990-2009 Adobe Systems Incorporated. +All rights reserved. See ./LICENSECNS2-H \ No newline at end of file diff --git a/src/main/resources/static/pdfjs/cmaps/ETenms-B5-H.bcmap b/src/main/resources/static/pdfjs/cmaps/ETenms-B5-H.bcmap index a7d69db5..c76f5f98 100644 --- a/src/main/resources/static/pdfjs/cmaps/ETenms-B5-H.bcmap +++ b/src/main/resources/static/pdfjs/cmaps/ETenms-B5-H.bcmap @@ -1,3 +1,3 @@ -RCopyright 1990-2009 Adobe Systems Incorporated. -All rights reserved. +RCopyright 1990-2009 Adobe Systems Incorporated. +All rights reserved. See ./LICENSE ETen-B5-H` ^ \ No newline at end of file From c55a5657a40970f777dee6a7080c1d9da60edbc9 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 30 Dec 2023 19:14:14 +0000 Subject: [PATCH 09/47] Add .git-blame-ignore-revs to ignore formatting commits in git blame --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..ce4c63a7 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Formatting +5f771b785130154ed47952635b7acef371ffe0ec \ No newline at end of file From b572a5e4c9887dcd1ce521e175c3a52ad70aec89 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 30 Dec 2023 19:37:46 +0000 Subject: [PATCH 10/47] auto run spotlessapply --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 95a0930d..94903c1c 100644 --- a/build.gradle +++ b/build.gradle @@ -150,6 +150,9 @@ dependencies { annotationProcessor 'org.projectlombok:lombok:1.18.28' } +tasks.withType(JavaCompile) { + dependsOn 'spotlessApply' +} task writeVersion { def propsFile = file('src/main/resources/version.properties') From cf640c7e3fc1443a296e26ede955a9bb271e20c6 Mon Sep 17 00:00:00 2001 From: sbplat <71648843+sbplat@users.noreply.github.com> Date: Sat, 30 Dec 2023 14:50:59 -0500 Subject: [PATCH 11/47] refactor: have a newline between annotations --- build.gradle | 1 - .../software/SPDF/config/security/SecurityConfiguration.java | 3 ++- .../SPDF/config/security/UserAuthenticationFilter.java | 3 ++- .../SPDF/config/security/UserBasedRateLimitingFilter.java | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 94903c1c..3bf48dcb 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,6 @@ spotless { importOrder('java', 'javax', 'org', 'com', 'net', 'io') toggleOffOn() - formatAnnotations() trimTrailingWhitespace() indentWithSpaces() endWithNewline() diff --git a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java index 8fd0b401..2b7ed6d1 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -35,7 +35,8 @@ public class SecurityConfiguration { @Autowired @Lazy private UserService userService; @Autowired - @Qualifier("loginEnabled") public boolean loginEnabledValue; + @Qualifier("loginEnabled") + public boolean loginEnabledValue; @Autowired private UserAuthenticationFilter userAuthenticationFilter; diff --git a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java index 6b8d047c..b4375568 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java @@ -28,7 +28,8 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { @Autowired @Lazy private UserService userService; @Autowired - @Qualifier("loginEnabled") public boolean loginEnabledValue; + @Qualifier("loginEnabled") + public boolean loginEnabledValue; @Override protected void doFilterInternal( diff --git a/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java index cadbfb24..6c315971 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java @@ -36,7 +36,8 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired - @Qualifier("rateLimit") public boolean rateLimit; + @Qualifier("rateLimit") + public boolean rateLimit; @Override protected void doFilterInternal( From eda91cc55610ed9731351e2e2a6a1c42f0199bab Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 30 Dec 2023 21:32:04 +0000 Subject: [PATCH 12/47] tests --- .../docker-compose-latest-lite-security.yml | 4 +-- .../docker-compose-latest-lite.yml | 4 +-- .../docker-compose-latest-security.yml | 4 +-- ...ker-compose-latest-ultra-lite-security.yml | 4 +-- .../docker-compose-latest-ultra-lite.yml | 4 +-- exampleYmlFiles/docker-compose-latest.yml | 4 +-- test.sh | 28 +++++++++++-------- 7 files changed, 28 insertions(+), 24 deletions(-) diff --git a/exampleYmlFiles/docker-compose-latest-lite-security.yml b/exampleYmlFiles/docker-compose-latest-lite-security.yml index 98b95058..cb82fdb0 100644 --- a/exampleYmlFiles/docker-compose-latest-lite-security.yml +++ b/exampleYmlFiles/docker-compose-latest-lite-security.yml @@ -9,9 +9,9 @@ services: memory: 1G healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"] - interval: 30s + interval: 5s timeout: 10s - retries: 3 + retries: 16 ports: - 8080:8080 volumes: diff --git a/exampleYmlFiles/docker-compose-latest-lite.yml b/exampleYmlFiles/docker-compose-latest-lite.yml index c2f18efe..920ae8d8 100644 --- a/exampleYmlFiles/docker-compose-latest-lite.yml +++ b/exampleYmlFiles/docker-compose-latest-lite.yml @@ -9,9 +9,9 @@ services: memory: 1G healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"] - interval: 20s + interval: 5s timeout: 10s - retries: 3 + retries: 16 ports: - 8080:8080 volumes: diff --git a/exampleYmlFiles/docker-compose-latest-security.yml b/exampleYmlFiles/docker-compose-latest-security.yml index b36a08a3..a480c27d 100644 --- a/exampleYmlFiles/docker-compose-latest-security.yml +++ b/exampleYmlFiles/docker-compose-latest-security.yml @@ -9,9 +9,9 @@ services: memory: 1G healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"] - interval: 20s + interval: 5s timeout: 10s - retries: 3 + retries: 16 ports: - 8080:8080 volumes: diff --git a/exampleYmlFiles/docker-compose-latest-ultra-lite-security.yml b/exampleYmlFiles/docker-compose-latest-ultra-lite-security.yml index 7b0d6c43..a8de634f 100644 --- a/exampleYmlFiles/docker-compose-latest-ultra-lite-security.yml +++ b/exampleYmlFiles/docker-compose-latest-ultra-lite-security.yml @@ -9,9 +9,9 @@ services: memory: 1G healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"] - interval: 30s + interval: 5s timeout: 10s - retries: 3 + retries: 16 ports: - 8080:8080 volumes: diff --git a/exampleYmlFiles/docker-compose-latest-ultra-lite.yml b/exampleYmlFiles/docker-compose-latest-ultra-lite.yml index 5848873d..b3ad7b00 100644 --- a/exampleYmlFiles/docker-compose-latest-ultra-lite.yml +++ b/exampleYmlFiles/docker-compose-latest-ultra-lite.yml @@ -9,9 +9,9 @@ services: memory: 1G healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"] - interval: 20s + interval: 5s timeout: 10s - retries: 3 + retries: 16 ports: - 8080:8080 volumes: diff --git a/exampleYmlFiles/docker-compose-latest.yml b/exampleYmlFiles/docker-compose-latest.yml index e319695f..21fff579 100644 --- a/exampleYmlFiles/docker-compose-latest.yml +++ b/exampleYmlFiles/docker-compose-latest.yml @@ -9,9 +9,9 @@ services: memory: 1G healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"] - interval: 30s + interval: 5s timeout: 10s - retries: 3 + retries: 16 ports: - 8080:8080 volumes: diff --git a/test.sh b/test.sh index 3b84ef42..9617ba14 100644 --- a/test.sh +++ b/test.sh @@ -6,16 +6,17 @@ check_health() { local compose_file=$2 local end=$((SECONDS+60)) - echo "Waiting for $service_name to become healthy..." - until [ "$(docker inspect --format='{{json .State.Health.Status}}' "$service_name")" == '"healthy"' ] || [ $SECONDS -ge $end ]; do - sleep 10 - echo "Waiting..." - if [ $SECONDS -ge $end ]; then - echo "$service_name health check timed out after 80 seconds." - return 1 - fi - done - echo "$service_name is healthy!" + echo -n "Waiting for $service_name to become healthy..." + until [ "$(docker inspect --format='{{json .State.Health.Status}}' "$service_name")" == '"healthy"' ] || [ $SECONDS -ge $end ]; do + sleep 3 + echo -n "." + if [ $SECONDS -ge $end ]; then + echo -e "\n$service_name health check timed out after 80 seconds." + return 1 + fi + done + echo -e "\n$service_name is healthy!" + return 0 } @@ -63,6 +64,8 @@ run_tests() { # Main testing routine main() { + SECONDS=0 + export DOCKER_ENABLE_SECURITY=false ./gradlew clean build @@ -90,8 +93,7 @@ main() { run_tests "Stirling-PDF-Security" "./exampleYmlFiles/docker-compose-latest-security.yml" # Report results - echo "All tests completed." - + echo "All tests completed in $SECONDS seconds." if [ ${#passed_tests[@]} -ne 0 ]; then @@ -108,6 +110,8 @@ main() { echo -e "\e[31m$test\e[0m" # Red color for failed tests done + + # Check if there are any failed tests and exit with an error code if so if [ ${#failed_tests[@]} -ne 0 ]; then echo "Some tests failed." From 7dfeb4bb0f1f7b81713376aadeb23d1668a62c78 Mon Sep 17 00:00:00 2001 From: sbplat <71648843+sbplat@users.noreply.github.com> Date: Sat, 30 Dec 2023 18:12:06 -0500 Subject: [PATCH 13/47] Update footer.html --- .../resources/templates/fragments/footer.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/resources/templates/fragments/footer.html b/src/main/resources/templates/fragments/footer.html index 0bff58e6..4fc411fe 100644 --- a/src/main/resources/templates/fragments/footer.html +++ b/src/main/resources/templates/fragments/footer.html @@ -1,9 +1,9 @@ -

-
- - - - -
Powered by Stirling PDF
+
+
+ + + + +
Powered by Stirling PDF
-
\ No newline at end of file +
From 5b0145fa478b914e7b82a7724b1eb4ad30945e55 Mon Sep 17 00:00:00 2001 From: sbplat <71648843+sbplat@users.noreply.github.com> Date: Sat, 30 Dec 2023 18:22:46 -0500 Subject: [PATCH 14/47] misc: change all repo links from Frooodle to Stirling-Tools --- .../SPDF/controller/api/misc/AutoSplitPdfController.java | 2 +- src/main/resources/static/js/githubVersion.js | 4 ++-- src/main/resources/templates/error.html | 2 +- src/main/resources/templates/fragments/errorBanner.html | 4 ++-- .../resources/templates/fragments/errorBannerPerPage.html | 4 ++-- src/main/resources/templates/fragments/navbar.html | 2 +- src/main/resources/templates/misc/ocr-pdf.html | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java index e32e325e..9b447fcb 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java @@ -42,7 +42,7 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "Misc", description = "Miscellaneous APIs") public class AutoSplitPdfController { - private static final String QR_CONTENT = "https://github.com/Frooodle/Stirling-PDF"; + private static final String QR_CONTENT = "https://github.com/Stirling-Tools/Stirling-PDF"; @PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data") @Operation( diff --git a/src/main/resources/static/js/githubVersion.js b/src/main/resources/static/js/githubVersion.js index 0fddc3ef..6400d5cf 100644 --- a/src/main/resources/static/js/githubVersion.js +++ b/src/main/resources/static/js/githubVersion.js @@ -18,7 +18,7 @@ function compareVersions(version1, version2) { async function getLatestReleaseVersion() { - const url = "https://api.github.com/repos/Frooodle/Stirling-PDF/releases/latest"; + const url = "https://api.github.com/repos/Stirling-Tools/Stirling-PDF/releases/latest"; try { const response = await fetch(url); const data = await response.json(); @@ -52,4 +52,4 @@ async function checkForUpdate() { document.addEventListener('DOMContentLoaded', (event) => { checkForUpdate(); -}); \ No newline at end of file +}); diff --git a/src/main/resources/templates/error.html b/src/main/resources/templates/error.html index dff3fab4..4e6fc585 100644 --- a/src/main/resources/templates/error.html +++ b/src/main/resources/templates/error.html @@ -115,7 +115,7 @@ margin-top: 0;

Need help / Found a issue?

If you're still having trouble, don't hesitate to reach out to us for help. You can submit a ticket on our GitHub page or contact us through Discord:

Go back to homepage diff --git a/src/main/resources/templates/fragments/errorBanner.html b/src/main/resources/templates/fragments/errorBanner.html index cdba311a..1bc79031 100644 --- a/src/main/resources/templates/fragments/errorBanner.html +++ b/src/main/resources/templates/fragments/errorBanner.html @@ -31,7 +31,7 @@

       
       
-      Submit a ticket on GitHub
+      Submit a ticket on GitHub
       Join our Discord server
     
   
@@ -84,4 +84,4 @@
       }
     }
   
-
\ No newline at end of file
+
diff --git a/src/main/resources/templates/fragments/errorBannerPerPage.html b/src/main/resources/templates/fragments/errorBannerPerPage.html
index ccc2f7b4..583d057b 100644
--- a/src/main/resources/templates/fragments/errorBannerPerPage.html
+++ b/src/main/resources/templates/fragments/errorBannerPerPage.html
@@ -43,7 +43,7 @@
 	            

Need help / Found an issue?

If you're still having trouble, don't hesitate to reach out to us for help. You can submit a ticket on our GitHub page or contact us through Discord:

Go to Homepage @@ -55,4 +55,4 @@ - \ No newline at end of file + diff --git a/src/main/resources/templates/fragments/navbar.html b/src/main/resources/templates/fragments/navbar.html index 2ddb3679..8e5ce732 100644 --- a/src/main/resources/templates/fragments/navbar.html +++ b/src/main/resources/templates/fragments/navbar.html @@ -267,7 +267,7 @@ - + diff --git a/src/main/resources/templates/misc/ocr-pdf.html b/src/main/resources/templates/misc/ocr-pdf.html index 440c42a1..c9063a9d 100644 --- a/src/main/resources/templates/misc/ocr-pdf.html +++ b/src/main/resources/templates/misc/ocr-pdf.html @@ -243,7 +243,7 @@

- https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md + https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md @@ -251,4 +251,4 @@
- \ No newline at end of file + From 3864e130cce1587966e66b358a74101edc38fead Mon Sep 17 00:00:00 2001 From: sbplat <71648843+sbplat@users.noreply.github.com> Date: Sat, 30 Dec 2023 18:36:07 -0500 Subject: [PATCH 15/47] misc: change all repo links from Frooodle to Stirling-Tools --- HowToAddNewLanguage.md | 8 ++++---- LocalRunGuide.md | 2 +- README.md | 26 +++++++++++++------------- chart/stirling-pdf/Chart.yaml | 8 ++++---- scripts/download-security-jar.sh | 10 +++++----- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/HowToAddNewLanguage.md b/HowToAddNewLanguage.md index b6a7fa4a..4cdcedfe 100644 --- a/HowToAddNewLanguage.md +++ b/HowToAddNewLanguage.md @@ -1,4 +1,4 @@ -


Stirling-PDF

+


Stirling-PDF

@@ -8,9 +8,9 @@ Fork Stirling-PDF and make a new branch out of Main Then add reference to the language in the navbar by adding a new language entry to the dropdown -https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html +https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html and add a flag svg file to -https://github.com/Frooodle/Stirling-PDF/tree/main/src/main/resources/static/images/flags +https://github.com/Stirling-Tools/Stirling-PDF/tree/main/src/main/resources/static/images/flags Any SVG flags are fine, i got most of mine from [here](https://flagicons.lipis.dev/) If your language isnt represented by a flag just find whichever closely matches it, such as for Arabic i chose Saudi Arabia @@ -25,7 +25,7 @@ The data-language-code is the code used to reference the file in the next step. Start by copying the existing english property file -[https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties](https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties) +[https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties) Copy and rename it to messages_{your data-language-code here}.properties, in the polish example you would set the name to messages_pl_PL.properties diff --git a/LocalRunGuide.md b/LocalRunGuide.md index c7eaca0b..824c1ae7 100644 --- a/LocalRunGuide.md +++ b/LocalRunGuide.md @@ -109,7 +109,7 @@ pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint ```bash cd ~/.git &&\ -git clone https://github.com/Frooodle/Stirling-PDF.git &&\ +git clone https://github.com/Stirling-Tools/Stirling-PDF.git &&\ cd Stirling-PDF &&\ chmod +x ./gradlew &&\ ./gradlew build diff --git a/README.md b/README.md index d9515ca6..f9b42a42 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ -


Stirling-PDF

+


Stirling-PDF

[![Docker Pulls](https://img.shields.io/docker/pulls/frooodle/s-pdf)](https://hub.docker.com/r/frooodle/s-pdf) [![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/Cn8pWhQRxZ) -[![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Frooodle/Stirling-PDF/) -[![GitHub Repo stars](https://img.shields.io/github/stars/frooodle/stirling-pdf?style=social)](https://github.com/Frooodle/stirling-pdf) +[![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/) +[![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](https://github.com/Stirling-Tools/stirling-pdf) [![Paypal Donate](https://img.shields.io/badge/Paypal%20Donate-yellow?style=flat&logo=paypal)](https://www.paypal.com/paypalme/froodleplex) [![Github Sponser](https://img.shields.io/badge/Github%20Sponsor-yellow?style=flat&logo=github)](https://github.com/sponsors/Frooodle) -[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Frooodle/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af) +[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af) This is a powerful locally hosted web based PDF manipulation tool using docker that allows you to perform various operations on PDF files, such as splitting merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application started as a 100% ChatGPT-made application and has evolved to include a wide range of features to handle all your PDF needs. @@ -22,10 +22,10 @@ Please feel free to submit feature requests or report bugs either through GitHub ## Features - Dark mode support. -- Custom download options (see [here](https://github.com/Frooodle/Stirling-PDF/blob/main/images/settings.png) for example) +- Custom download options (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/images/settings.png) for example) - Parallel file processing and downloads - API for integration with external scripts -- Optional Login and Authentication support (see [here](https://github.com/Frooodle/Stirling-PDF/tree/main#login-authentication) for documentation) +- Optional Login and Authentication support (see [here](https://github.com/Stirling-Tools/Stirling-PDF/tree/main#login-authentication) for documentation) ## **PDF Features** @@ -80,7 +80,7 @@ Please feel free to submit feature requests or report bugs either through GitHub - Get all information on a PDF to view or export as JSON. -For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Frooodle/Stirling-PDF/blob/main/Endpoint-groups.md) +For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md) Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) hosted by the team at adminforge.de ## Technologies used @@ -96,13 +96,13 @@ Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) h ## How to use ### Locally -Please view https://github.com/Frooodle/Stirling-PDF/blob/main/LocalRunGuide.md +Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LocalRunGuide.md ### Docker / Podman https://hub.docker.com/r/frooodle/s-pdf Stirling PDF has 3 different versions, a Full version, Lite, and ultra-Lite. Depending on the types of features you use you may want a smaller image to save on space. -To see what the different versions offer please look at our [version mapping](https://github.com/Frooodle/Stirling-PDF/blob/main/Version-groups.md) +To see what the different versions offer please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md) For people that don't mind about space optimization just use the latest tag. ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest?label=Stirling-PDF%20Full) ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest-lite?label=Stirling-PDF%20Lite) @@ -144,7 +144,7 @@ services: Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "podman". ## Enable OCR/Compression feature -Please view https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md +Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md ## Want to add your own language? Stirling PDF currently supports 21! @@ -172,7 +172,7 @@ Stirling PDF currently supports 21! - Hindi (हिंदी) (hi_IN) If you want to add your own language to Stirling-PDF please refer -https://github.com/Frooodle/Stirling-PDF/blob/main/HowToAddNewLanguage.md +https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md And please create a PR to merge it back in so others can use it! @@ -224,7 +224,7 @@ metrics: enabled: true # 'true' to enable Info APIs endpoints (view http://localhost:8080/swagger-ui/index.html#/API to learn more), 'false' to disable ``` ### Extra notes -- Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Frooodle/Stirling-PDF/blob/main/Endpoint-groups.md) +- Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md) - customStaticFilePath. Customise static files such as the app logo by placing files in the /customFiles/static/ directory. An example of customising app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF ### Environment only parameters @@ -234,7 +234,7 @@ metrics: ## API For those wanting to use Stirling-PDFs backend API to link with their own custom scripting to edit PDFs you can view all existing API documentation -[here](https://app.swaggerhub.com/apis-docs/Frooodle/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF) +[here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF) ## Login authentication diff --git a/chart/stirling-pdf/Chart.yaml b/chart/stirling-pdf/Chart.yaml index a69894a0..686d2422 100644 --- a/chart/stirling-pdf/Chart.yaml +++ b/chart/stirling-pdf/Chart.yaml @@ -1,15 +1,15 @@ apiVersion: v2 appVersion: 0.14.2 description: locally hosted web application that allows you to perform various operations on PDF files -home: https://github.com/Frooodle/Stirling-PDF +home: https://github.com/Stirling-Tools/Stirling-PDF keywords: - stirling-pdf - helm - charts repo maintainers: -- name: Frooodle - url: https://github.com/Frooodle/Stirling-PDF +- name: Stirling-Tools + url: https://github.com/Stirling-Tools/Stirling-PDF name: stirling-pdf-chart sources: -- https://github.com/Frooodle/Stirling-PDF +- https://github.com/Stirling-Tools/Stirling-PDF version: 1.0.0 diff --git a/scripts/download-security-jar.sh b/scripts/download-security-jar.sh index c0b710ad..64a5fa25 100644 --- a/scripts/download-security-jar.sh +++ b/scripts/download-security-jar.sh @@ -2,13 +2,13 @@ echo "Running Stirling PDF with DOCKER_ENABLE_SECURITY=${DOCKER_ENABLE_SECURITY} # Check for DOCKER_ENABLE_SECURITY and download the appropriate JAR if required if [ "$DOCKER_ENABLE_SECURITY" = "true" ] && [ "$VERSION_TAG" != "alpha" ]; then if [ ! -f app-security.jar ]; then - echo "Trying to download from: https://github.com/Frooodle/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar" - curl -L -o app-security.jar https://github.com/Frooodle/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar + echo "Trying to download from: https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar" + curl -L -o app-security.jar https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar # If the first download attempt failed, try with the 'v' prefix if [ $? -ne 0 ]; then - echo "Trying to download from: https://github.com/Frooodle/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar" - curl -L -o app-security.jar https://github.com/Frooodle/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar + echo "Trying to download from: https://github.com/Stirling-Tools/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar" + curl -L -o app-security.jar https://github.com/Stirling-Tools/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar fi if [ $? -eq 0 ]; then # checks if curl was successful @@ -16,4 +16,4 @@ if [ "$DOCKER_ENABLE_SECURITY" = "true" ] && [ "$VERSION_TAG" != "alpha" ]; then ln -s app-security.jar app.jar fi fi -fi \ No newline at end of file +fi From f535387ac4dc943eb0675a1ca02849b243e9fe6b Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sun, 31 Dec 2023 13:05:38 +0000 Subject: [PATCH 16/47] pipeline enhance for MI --- .../api/pipeline/PipelineProcessor.java | 4 +- src/main/resources/static/js/pipeline.js | 997 +++++++++--------- src/main/resources/templates/pipeline.html | 2 + 3 files changed, 521 insertions(+), 482 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java index 8b4b2ef4..c2841a05 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java @@ -138,7 +138,7 @@ public class PipelineProcessor { hasErrors = true; } - outputFiles = newOutputFiles; + } } else { @@ -177,11 +177,13 @@ public class PipelineProcessor { } } logPrintStream.close(); + outputFiles = newOutputFiles; } if (hasErrors) { logger.error("Errors occurred during processing. Log: {}", logStream.toString()); } + return outputFiles; } diff --git a/src/main/resources/static/js/pipeline.js b/src/main/resources/static/js/pipeline.js index 4fcde3a0..8db09c48 100644 --- a/src/main/resources/static/js/pipeline.js +++ b/src/main/resources/static/js/pipeline.js @@ -1,490 +1,511 @@ -document.getElementById('validateButton').addEventListener('click', function(event) { - event.preventDefault(); - validatePipeline(); -}); -function validatePipeline() { - let pipelineListItems = document.getElementById('pipelineList').children; - let isValid = true; - let containsAddPassword = false; - for (let i = 0; i < pipelineListItems.length - 1; i++) { - let currentOperation = pipelineListItems[i].querySelector('.operationName').textContent; - let nextOperation = pipelineListItems[i + 1].querySelector('.operationName').textContent; - if (currentOperation === '/add-password') { - containsAddPassword = true; - } - - let currentOperationDescription = apiDocs[currentOperation]?.post?.description || ""; - let nextOperationDescription = apiDocs[nextOperation]?.post?.description || ""; - - // Strip off 'ZIP-' prefix - currentOperationDescription = currentOperationDescription.replace("ZIP-", ''); - nextOperationDescription = nextOperationDescription.replace("ZIP-", ''); - - let currentOperationOutput = currentOperationDescription.match(/Output:([A-Z\/]*)/)?.[1] || ""; - let nextOperationInput = nextOperationDescription.match(/Input:([A-Z\/]*)/)?.[1] || ""; - - // Splitting in case of multiple possible output/input - let currentOperationOutputArr = currentOperationOutput.split('/'); - let nextOperationInputArr = nextOperationInput.split('/'); - - if (currentOperationOutput !== 'ANY' && nextOperationInput !== 'ANY') { - let intersection = currentOperationOutputArr.filter(value => nextOperationInputArr.includes(value)); - console.log(`Intersection: ${intersection}`); - - if (intersection.length === 0) { - updateValidateButton(false); - isValid = false; - console.log(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`); - alert(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`); - break; - } - } - } - if (containsAddPassword && pipelineListItems[pipelineListItems.length - 1].querySelector('.operationName').textContent !== '/add-password') { - updateValidateButton(false); - alert('The "add-password" operation should be at the end of the operations sequence. Please adjust the operations order.'); - return false; - } - if (isValid) { - console.log('Pipeline is valid'); - // Continue with the pipeline operation - } else { - console.error('Pipeline is not valid'); - // Stop operation, maybe display an error to the user - } - updateValidateButton(isValid); - return isValid; -} - -function updateValidateButton(isValid) { - var validateButton = document.getElementById('validateButton'); - if (isValid) { - validateButton.classList.remove('btn-danger'); - validateButton.classList.add('btn-success'); - } else { - validateButton.classList.remove('btn-success'); - validateButton.classList.add('btn-danger'); - } -} - - - - -document.getElementById('submitConfigBtn').addEventListener('click', function() { - - if (validatePipeline() === false) { - return; - } - let selectedOperation = document.getElementById('operationsDropdown').value; - - - - var pipelineName = document.getElementById('pipelineName').value; - let pipelineList = document.getElementById('pipelineList').children; - let pipelineConfig = { - "name": pipelineName, - "pipeline": [], - "_examples": { - "outputDir": "{outputFolder}/{folderName}", - "outputFileName": "{filename}-{pipelineName}-{date}-{time}" - }, - "outputDir": "httpWebRequest", - "outputFileName": "{filename}" - }; - - for (let i = 0; i < pipelineList.length; i++) { - let operationName = pipelineList[i].querySelector('.operationName').textContent; - let parameters = operationSettings[operationName] || {}; - - pipelineConfig.pipeline.push({ - "operation": operationName, - "parameters": parameters - }); - } - - - - - - - - - - - - - - let pipelineConfigJson = JSON.stringify(pipelineConfig, null, 2); - - let formData = new FormData(); - - let fileInput = document.getElementById('fileInput-input'); - let files = fileInput.files; - - for (let i = 0; i < files.length; i++) { - console.log("files[i]", files[i].name); - formData.append('fileInput', files[i], files[i].name); - } - - console.log("pipelineConfigJson", pipelineConfigJson); - formData.append('json', pipelineConfigJson); - console.log("formData", formData); - - fetch('api/v1/pipeline/handleData', { - method: 'POST', - body: formData - }) - .then(response => { - // Save the response to use it later - const responseToUseLater = response; - - return response.blob().then(blob => { - let url = window.URL.createObjectURL(blob); - let a = document.createElement('a'); - a.href = url; - - // Use responseToUseLater instead of response - const contentDisposition = responseToUseLater.headers.get('Content-Disposition'); - let filename = 'download'; - if (contentDisposition && contentDisposition.indexOf('attachment') !== -1) { - filename = decodeURIComponent(contentDisposition.split('filename=')[1].replace(/"/g, '')).trim(); - } - a.download = filename; - - document.body.appendChild(a); - a.click(); - a.remove(); - }); - }) - .catch((error) => { - console.error('Error:', error); + document.getElementById('validateButton').addEventListener('click', function(event) { + event.preventDefault(); + validatePipeline(); }); - -}); - -let apiDocs = {}; -let apiSchemas = {}; -let operationSettings = {}; - -fetch('v1/api-docs') - .then(response => response.json()) - .then(data => { - - apiDocs = data.paths; - apiSchemas = data.components.schemas; - let operationsDropdown = document.getElementById('operationsDropdown'); - const ignoreOperations = ["/api/v1/pipeline/handleData", "/api/v1/pipeline/operationToIgnore"]; // Add the operations you want to ignore here - - operationsDropdown.innerHTML = ''; - - let operationsByTag = {}; - - // Group operations by tags - Object.keys(data.paths).forEach(operationPath => { - let operation = data.paths[operationPath].post; - if(!operation || !operation.description) { - console.log(operationPath); + function validatePipeline() { + let pipelineListItems = document.getElementById('pipelineList').children; + let isValid = true; + let containsAddPassword = false; + for (let i = 0; i < pipelineListItems.length - 1; i++) { + let currentOperation = pipelineListItems[i].querySelector('.operationName').textContent; + let nextOperation = pipelineListItems[i + 1].querySelector('.operationName').textContent; + if (currentOperation === '/add-password') { + containsAddPassword = true; } - if (operation && !ignoreOperations.includes(operationPath) && !operation.description.includes("Type:MISO")) { - let operationTag = operation.tags[0]; // This assumes each operation has exactly one tag - if (!operationsByTag[operationTag]) { - operationsByTag[operationTag] = []; + + let currentOperationDescription = apiDocs[currentOperation]?.post?.description || ""; + let nextOperationDescription = apiDocs[nextOperation]?.post?.description || ""; + + // Strip off 'ZIP-' prefix + currentOperationDescription = currentOperationDescription.replace("ZIP-", ''); + nextOperationDescription = nextOperationDescription.replace("ZIP-", ''); + + let currentOperationOutput = currentOperationDescription.match(/Output:([A-Z\/]*)/)?.[1] || ""; + let nextOperationInput = nextOperationDescription.match(/Input:([A-Z\/]*)/)?.[1] || ""; + + // Splitting in case of multiple possible output/input + let currentOperationOutputArr = currentOperationOutput.split('/'); + let nextOperationInputArr = nextOperationInput.split('/'); + + if (currentOperationOutput !== 'ANY' && nextOperationInput !== 'ANY') { + let intersection = currentOperationOutputArr.filter(value => nextOperationInputArr.includes(value)); + console.log(`Intersection: ${intersection}`); + + if (intersection.length === 0) { + updateValidateButton(false); + isValid = false; + console.log(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`); + alert(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`); + break; } - operationsByTag[operationTag].push(operationPath); } - }); - // Specify the order of tags - let tagOrder = ["General", "Security", "Convert", "Misc", "Filter"]; - - // Create dropdown options - tagOrder.forEach(tag => { - if (operationsByTag[tag]) { - let group = document.createElement('optgroup'); - group.label = tag; - - operationsByTag[tag].forEach(operationPath => { - let option = document.createElement('option'); - - let operationPathDisplay = operationPath - operationPathDisplay = operationPath.replace(new RegExp("api/v1/" + tag.toLowerCase() + "/", 'i'), ""); - - - if(operationPath.includes("/convert")){ - operationPathDisplay = operationPathDisplay.replace(/^\//, '').replaceAll("/", " to "); - } else { - operationPathDisplay = operationPathDisplay.replace(/\//g, ''); // Remove slashes - } - operationPathDisplay = operationPathDisplay.replaceAll(" ","-"); - option.textContent = operationPathDisplay; - option.value = operationPath; // Keep the value with slashes for querying - group.appendChild(option); - }); - - operationsDropdown.appendChild(group); - } - }); - }); - - -document.getElementById('addOperationBtn').addEventListener('click', function() { - let selectedOperation = document.getElementById('operationsDropdown').value; - let pipelineList = document.getElementById('pipelineList'); - - let listItem = document.createElement('li'); - listItem.className = "list-group-item"; - let hasSettings = false; - if (apiDocs[selectedOperation] && apiDocs[selectedOperation].post) { - const postMethod = apiDocs[selectedOperation].post; - - // Check if parameters exist - if (postMethod.parameters && postMethod.parameters.length > 0) { - hasSettings = true; - } else if (postMethod.requestBody && postMethod.requestBody.content['multipart/form-data']) { - // Extract the reference key - const refKey = postMethod.requestBody.content['multipart/form-data'].schema['$ref'].split('/').pop(); - // Check if the referenced schema exists and has properties - if (apiSchemas[refKey] && Object.keys(apiSchemas[refKey].properties).length > 0) { - hasSettings = true; - } - } + } + if (containsAddPassword && pipelineListItems[pipelineListItems.length - 1].querySelector('.operationName').textContent !== '/add-password') { + updateValidateButton(false); + alert('The "add-password" operation should be at the end of the operations sequence. Please adjust the operations order.'); + return false; + } + if (isValid) { + console.log('Pipeline is valid'); + // Continue with the pipeline operation + } else { + console.error('Pipeline is not valid'); + // Stop operation, maybe display an error to the user + } + updateValidateButton(isValid); + return isValid; } - - - - - listItem.innerHTML = ` -
-
${selectedOperation}
-
- - - - -
-
-`; - - - pipelineList.appendChild(listItem); - - listItem.querySelector('.move-up').addEventListener('click', function(event) { - event.preventDefault(); - if (listItem.previousElementSibling) { - pipelineList.insertBefore(listItem, listItem.previousElementSibling); + + function updateValidateButton(isValid) { + var validateButton = document.getElementById('validateButton'); + if (isValid) { + validateButton.classList.remove('btn-danger'); + validateButton.classList.add('btn-success'); + } else { + validateButton.classList.remove('btn-success'); + validateButton.classList.add('btn-danger'); } - }); - - listItem.querySelector('.move-down').addEventListener('click', function(event) { - event.preventDefault(); - if (listItem.nextElementSibling) { - pipelineList.insertBefore(listItem.nextElementSibling, listItem); + } + + + + + document.getElementById('submitConfigBtn').addEventListener('click', function() { + + if (validatePipeline() === false) { + return; } - }); - - listItem.querySelector('.remove').addEventListener('click', function(event) { - event.preventDefault(); - pipelineList.removeChild(listItem); - hideOrShowPipelineHeader(); - }); - - listItem.querySelector('.pipelineSettings').addEventListener('click', function(event) { - event.preventDefault(); - showpipelineSettingsModal(selectedOperation); - hideOrShowPipelineHeader(); - }); - - function showpipelineSettingsModal(operation) { - let pipelineSettingsModal = document.getElementById('pipelineSettingsModal'); - let pipelineSettingsContent = document.getElementById('pipelineSettingsContent'); - let operationData = apiDocs[operation].post.parameters || []; - - // Resolve the $ref reference to get actual schema properties - let refKey = apiDocs[operation].post.requestBody.content['multipart/form-data'].schema['$ref'].split('/').pop(); - let requestBodyData = apiSchemas[refKey].properties || {}; - - // Combine operationData and requestBodyData into a single array - operationData = operationData.concat(Object.keys(requestBodyData).map(key => ({ - name: key, - schema: requestBodyData[key] - }))); - - pipelineSettingsContent.innerHTML = ''; - - operationData.forEach(parameter => { - // If the parameter name is 'fileInput', return early to skip the rest of this iteration - if (parameter.name === 'fileInput') return; - - let parameterDiv = document.createElement('div'); - parameterDiv.className = "mb-3"; - - let parameterLabel = document.createElement('label'); - parameterLabel.textContent = `${parameter.name} (${parameter.schema.type}): `; - parameterLabel.title = parameter.schema.description; - parameterLabel.setAttribute('for', parameter.name); - parameterDiv.appendChild(parameterLabel); - - let defaultValue = parameter.schema.example; - if (defaultValue === undefined) defaultValue = parameter.schema.default; - - let parameterInput; - - // check if enum exists in schema - if (parameter.schema.enum) { - // if enum exists, create a select element - parameterInput = document.createElement('select'); - parameterInput.className = "form-control"; - - // iterate over each enum value and create an option for it - parameter.schema.enum.forEach(value => { - let option = document.createElement('option'); - option.value = value; - option.text = value; - parameterInput.appendChild(option); + let selectedOperation = document.getElementById('operationsDropdown').value; + + + + var pipelineName = document.getElementById('pipelineName').value; + let pipelineList = document.getElementById('pipelineList').children; + let pipelineConfig = { + "name": pipelineName, + "pipeline": [], + "_examples": { + "outputDir": "{outputFolder}/{folderName}", + "outputFileName": "{filename}-{pipelineName}-{date}-{time}" + }, + "outputDir": "httpWebRequest", + "outputFileName": "{filename}" + }; + + for (let i = 0; i < pipelineList.length; i++) { + let operationName = pipelineList[i].querySelector('.operationName').textContent; + let parameters = operationSettings[operationName] || {}; + + pipelineConfig.pipeline.push({ + "operation": operationName, + "parameters": parameters + }); + } + + + + + + + + + + + + + + let pipelineConfigJson = JSON.stringify(pipelineConfig, null, 2); + + let formData = new FormData(); + + let fileInput = document.getElementById('fileInput-input'); + let files = fileInput.files; + + for (let i = 0; i < files.length; i++) { + console.log("files[i]", files[i].name); + formData.append('fileInput', files[i], files[i].name); + } + + console.log("pipelineConfigJson", pipelineConfigJson); + formData.append('json', pipelineConfigJson); + console.log("formData", formData); + + fetch('api/v1/pipeline/handleData', { + method: 'POST', + body: formData + }) + .then(response => { + // Save the response to use it later + const responseToUseLater = response; + + return response.blob().then(blob => { + let url = window.URL.createObjectURL(blob); + let a = document.createElement('a'); + a.href = url; + + // Use responseToUseLater instead of response + const contentDisposition = responseToUseLater.headers.get('Content-Disposition'); + let filename = 'download'; + if (contentDisposition && contentDisposition.indexOf('attachment') !== -1) { + filename = decodeURIComponent(contentDisposition.split('filename=')[1].replace(/"/g, '')).trim(); + } + a.download = filename; + + document.body.appendChild(a); + a.click(); + a.remove(); }); - } else { - // switch-case statement for handling non-enum types - switch (parameter.schema.type) { - case 'string': - if (parameter.schema.format === 'binary') { - // This is a file input - - //parameterInput = document.createElement('input'); - //parameterInput.type = 'file'; - //parameterInput.className = "form-control"; - - parameterInput = document.createElement('input'); - parameterInput.type = 'text'; - parameterInput.className = "form-control"; - parameterInput.value = "FileInputPathToBeInputtedManuallyOffline"; + }) + .catch((error) => { + console.error('Error:', error); + }); + + }); + + let apiDocs = {}; + let apiSchemas = {}; + let operationSettings = {}; + + fetch('v1/api-docs') + .then(response => response.json()) + .then(data => { + + apiDocs = data.paths; + apiSchemas = data.components.schemas; + let operationsDropdown = document.getElementById('operationsDropdown'); + const ignoreOperations = ["/api/v1/pipeline/handleData", "/api/v1/pipeline/operationToIgnore"]; // Add the operations you want to ignore here + + operationsDropdown.innerHTML = ''; + + let operationsByTag = {}; + + // Group operations by tags + Object.keys(data.paths).forEach(operationPath => { + let operation = data.paths[operationPath].post; + if (!operation || !operation.description) { + console.log(operationPath); + } + //!operation.description.includes("Type:MISO") + if (operation && !ignoreOperations.includes(operationPath)) { + let operationTag = operation.tags[0]; // This assumes each operation has exactly one tag + if (!operationsByTag[operationTag]) { + operationsByTag[operationTag] = []; + } + operationsByTag[operationTag].push(operationPath); + } + }); + // Specify the order of tags + let tagOrder = ["General", "Security", "Convert", "Misc", "Filter"]; + + // Create dropdown options + tagOrder.forEach(tag => { + if (operationsByTag[tag]) { + let group = document.createElement('optgroup'); + group.label = tag; + + operationsByTag[tag].forEach(operationPath => { + let option = document.createElement('option'); + + let operationPathDisplay = operationPath + operationPathDisplay = operationPath.replace(new RegExp("api/v1/" + tag.toLowerCase() + "/", 'i'), ""); + + + if (operationPath.includes("/convert")) { + operationPathDisplay = operationPathDisplay.replace(/^\//, '').replaceAll("/", " to "); } else { + operationPathDisplay = operationPathDisplay.replace(/\//g, ''); // Remove slashes + } + operationPathDisplay = operationPathDisplay.replaceAll(" ", "-"); + option.textContent = operationPathDisplay; + option.value = operationPath; // Keep the value with slashes for querying + group.appendChild(option); + }); + + operationsDropdown.appendChild(group); + } + }); + }); + + + document.getElementById('addOperationBtn').addEventListener('click', function() { + let selectedOperation = document.getElementById('operationsDropdown').value; + let pipelineList = document.getElementById('pipelineList'); + + let listItem = document.createElement('li'); + listItem.className = "list-group-item"; + let hasSettings = false; + if (apiDocs[selectedOperation] && apiDocs[selectedOperation].post) { + const postMethod = apiDocs[selectedOperation].post; + + // Check if parameters exist + if (postMethod.parameters && postMethod.parameters.length > 0) { + hasSettings = true; + } else if (postMethod.requestBody && postMethod.requestBody.content['multipart/form-data']) { + // Extract the reference key + const refKey = postMethod.requestBody.content['multipart/form-data'].schema['$ref'].split('/').pop(); + // Check if the referenced schema exists and has properties + if (apiSchemas[refKey] && Object.keys(apiSchemas[refKey].properties).length > 0) { + hasSettings = true; + } + } + } + + + + + listItem.innerHTML = ` +
+
${selectedOperation}
+
+ + + + +
+
+ `; + + + pipelineList.appendChild(listItem); + + listItem.querySelector('.move-up').addEventListener('click', function(event) { + event.preventDefault(); + if (listItem.previousElementSibling) { + pipelineList.insertBefore(listItem, listItem.previousElementSibling); + updateConfigInDropdown(); + } + }); + + listItem.querySelector('.move-down').addEventListener('click', function(event) { + event.preventDefault(); + if (listItem.nextElementSibling) { + pipelineList.insertBefore(listItem.nextElementSibling, listItem); + updateConfigInDropdown(); + } + + }); + + listItem.querySelector('.remove').addEventListener('click', function(event) { + event.preventDefault(); + pipelineList.removeChild(listItem); + hideOrShowPipelineHeader(); + updateConfigInDropdown(); + }); + + listItem.querySelector('.pipelineSettings').addEventListener('click', function(event) { + event.preventDefault(); + showpipelineSettingsModal(selectedOperation); + hideOrShowPipelineHeader(); + }); + + function showpipelineSettingsModal(operation) { + let pipelineSettingsModal = document.getElementById('pipelineSettingsModal'); + let pipelineSettingsContent = document.getElementById('pipelineSettingsContent'); + let operationData = apiDocs[operation].post.parameters || []; + + // Resolve the $ref reference to get actual schema properties + let refKey = apiDocs[operation].post.requestBody.content['multipart/form-data'].schema['$ref'].split('/').pop(); + let requestBodyData = apiSchemas[refKey].properties || {}; + + // Combine operationData and requestBodyData into a single array + operationData = operationData.concat(Object.keys(requestBodyData).map(key => ({ + name: key, + schema: requestBodyData[key] + }))); + + pipelineSettingsContent.innerHTML = ''; + + operationData.forEach(parameter => { + // If the parameter name is 'fileInput', return early to skip the rest of this iteration + if (parameter.name === 'fileInput') return; + + let parameterDiv = document.createElement('div'); + parameterDiv.className = "mb-3"; + + let parameterLabel = document.createElement('label'); + parameterLabel.textContent = `${parameter.name} (${parameter.schema.type}): `; + parameterLabel.title = parameter.schema.description; + parameterLabel.setAttribute('for', parameter.name); + parameterDiv.appendChild(parameterLabel); + + let defaultValue = parameter.schema.example; + if (defaultValue === undefined) defaultValue = parameter.schema.default; + + let parameterInput; + + // check if enum exists in schema + if (parameter.schema.enum) { + // if enum exists, create a select element + parameterInput = document.createElement('select'); + parameterInput.className = "form-control"; + + // iterate over each enum value and create an option for it + parameter.schema.enum.forEach(value => { + let option = document.createElement('option'); + option.value = value; + option.text = value; + parameterInput.appendChild(option); + }); + } else { + // switch-case statement for handling non-enum types + switch (parameter.schema.type) { + case 'string': + if (parameter.schema.format === 'binary') { + // This is a file input + + //parameterInput = document.createElement('input'); + //parameterInput.type = 'file'; + //parameterInput.className = "form-control"; + + parameterInput = document.createElement('input'); + parameterInput.type = 'text'; + parameterInput.className = "form-control"; + parameterInput.value = "FileInputPathToBeInputtedManuallyOffline"; + } else { + parameterInput = document.createElement('input'); + parameterInput.type = 'text'; + parameterInput.className = "form-control"; + if (defaultValue !== undefined) parameterInput.value = defaultValue; + } + break; + case 'number': + case 'integer': + parameterInput = document.createElement('input'); + parameterInput.type = 'number'; + parameterInput.className = "form-control"; + if (defaultValue !== undefined) parameterInput.value = defaultValue; + break; + case 'boolean': + parameterInput = document.createElement('input'); + parameterInput.type = 'checkbox'; + if (defaultValue === true) parameterInput.checked = true; + break; + case 'array': + case 'object': + parameterInput = document.createElement('textarea'); + parameterInput.placeholder = `Enter a JSON formatted ${parameter.schema.type}`; + parameterInput.className = "form-control"; + break; + default: parameterInput = document.createElement('input'); parameterInput.type = 'text'; parameterInput.className = "form-control"; if (defaultValue !== undefined) parameterInput.value = defaultValue; - } - break; - case 'number': - case 'integer': - parameterInput = document.createElement('input'); - parameterInput.type = 'number'; - parameterInput.className = "form-control"; - if (defaultValue !== undefined) parameterInput.value = defaultValue; - break; - case 'boolean': - parameterInput = document.createElement('input'); - parameterInput.type = 'checkbox'; - if (defaultValue === true) parameterInput.checked = true; - break; - case 'array': - case 'object': - parameterInput = document.createElement('textarea'); - parameterInput.placeholder = `Enter a JSON formatted ${parameter.schema.type}`; - parameterInput.className = "form-control"; - break; - default: - parameterInput = document.createElement('input'); - parameterInput.type = 'text'; - parameterInput.className = "form-control"; - if (defaultValue !== undefined) parameterInput.value = defaultValue; + } } - } - parameterInput.id = parameter.name; - - console.log("defaultValue", defaultValue); - console.log("parameterInput", parameterInput); - if (operationSettings[operation] && operationSettings[operation][parameter.name] !== undefined) { - let savedValue = operationSettings[operation][parameter.name]; - - switch (parameter.schema.type) { - case 'number': - case 'integer': - parameterInput.value = savedValue.toString(); - break; - case 'boolean': - parameterInput.checked = savedValue; - break; - case 'array': - case 'object': - parameterInput.value = JSON.stringify(savedValue); - break; - default: - parameterInput.value = savedValue; - } - } - console.log("parameterInput2", parameterInput); - parameterDiv.appendChild(parameterInput); - - pipelineSettingsContent.appendChild(parameterDiv); - }); - - let saveButton = document.createElement('button'); - saveButton.textContent = "Save Settings"; - saveButton.className = "btn btn-primary"; - saveButton.addEventListener('click', function(event) { - event.preventDefault(); - let settings = {}; - operationData.forEach(parameter => { - if(parameter.name !== "fileInput"){ - let value = document.getElementById(parameter.name).value; + parameterInput.id = parameter.name; + + console.log("defaultValue", defaultValue); + console.log("parameterInput", parameterInput); + if (operationSettings[operation] && operationSettings[operation][parameter.name] !== undefined) { + let savedValue = operationSettings[operation][parameter.name]; + switch (parameter.schema.type) { case 'number': case 'integer': - settings[parameter.name] = Number(value); + parameterInput.value = savedValue.toString(); break; case 'boolean': - settings[parameter.name] = document.getElementById(parameter.name).checked; + parameterInput.checked = savedValue; break; case 'array': case 'object': - try { - settings[parameter.name] = JSON.parse(value); - } catch (err) { - console.error(`Invalid JSON format for ${parameter.name}`); - } + parameterInput.value = JSON.stringify(savedValue); break; default: - settings[parameter.name] = value; + parameterInput.value = savedValue; } } + console.log("parameterInput2", parameterInput); + parameterDiv.appendChild(parameterInput); + + pipelineSettingsContent.appendChild(parameterDiv); }); - operationSettings[operation] = settings; - //pipelineSettingsModal.style.display = "none"; - }); - pipelineSettingsContent.appendChild(saveButton); - - //pipelineSettingsModal.style.display = "block"; - - //pipelineSettingsModal.getElementsByClassName("close")[0].onclick = function() { - // pipelineSettingsModal.style.display = "none"; - //} - - //window.onclick = function(event) { - // if (event.target == pipelineSettingsModal) { - // pipelineSettingsModal.style.display = "none"; - // } - //} + + let saveButton = document.createElement('button'); + saveButton.textContent = "Save Settings"; + saveButton.className = "btn btn-primary"; + saveButton.addEventListener('click', function(event) { + event.preventDefault(); + let settings = {}; + operationData.forEach(parameter => { + if (parameter.name !== "fileInput") { + let value = document.getElementById(parameter.name).value; + switch (parameter.schema.type) { + case 'number': + case 'integer': + settings[parameter.name] = Number(value); + break; + case 'boolean': + settings[parameter.name] = document.getElementById(parameter.name).checked; + break; + case 'array': + case 'object': + try { + settings[parameter.name] = JSON.parse(value); + } catch (err) { + console.error(`Invalid JSON format for ${parameter.name}`); + } + break; + default: + settings[parameter.name] = value; + } + } + }); + operationSettings[operation] = settings; + //pipelineSettingsModal.style.display = "none"; + }); + pipelineSettingsContent.appendChild(saveButton); + + //pipelineSettingsModal.style.display = "block"; + + //pipelineSettingsModal.getElementsByClassName("close")[0].onclick = function() { + // pipelineSettingsModal.style.display = "none"; + //} + + //window.onclick = function(event) { + // if (event.target == pipelineSettingsModal) { + // pipelineSettingsModal.style.display = "none"; + // } + //} + } + updateConfigInDropdown(); + hideOrShowPipelineHeader(); + }); + + function updateConfigInDropdown() { + let pipelineSelect = document.getElementById('pipelineSelect'); + let selectedOption = pipelineSelect.options[pipelineSelect.selectedIndex]; + + // Get the current configuration as JSON + let pipelineConfigJson = configToJson(); + console.log("pipelineConfigJson", pipelineConfigJson); + if (!pipelineConfigJson) { + console.error("Failed to update configuration: Invalid configuration"); + return; + } + + // Update the value of the selected option with the new configuration + selectedOption.value = pipelineConfigJson; + } - hideOrShowPipelineHeader(); -}); - - var saveBtn = document.getElementById('savePipelineBtn'); - + // Remove any existing event listeners saveBtn.removeEventListener('click', savePipeline); // Add the event listener saveBtn.addEventListener('click', savePipeline); console.log("saveBtn", saveBtn) - function savePipeline() { - - if (validatePipeline() === false) { - return; + + function configToJson() { + if (!validatePipeline()) { + return null; // Return null if validation fails } - + var pipelineName = document.getElementById('pipelineName').value; let pipelineList = document.getElementById('pipelineList').children; let pipelineConfig = { @@ -497,35 +518,49 @@ document.getElementById('addOperationBtn').addEventListener('click', function() "outputDir": "{outputFolder}", "outputFileName": "{filename}" }; - + for (let i = 0; i < pipelineList.length; i++) { let operationName = pipelineList[i].querySelector('.operationName').textContent; let parameters = operationSettings[operationName] || {}; - + parameters['fileInput'] = 'automated'; - + pipelineConfig.pipeline.push({ "operation": operationName, "parameters": parameters }); } - console.log("Downloading.."); + + return JSON.stringify(pipelineConfig, null, 2); + } + + + + function savePipeline() { + let pipelineConfigJson = configToJson(); + if (!pipelineConfigJson) { + console.error("Failed to save pipeline: Invalid configuration"); + return; + } + + let pipelineName = document.getElementById('pipelineName').value; + console.log("Downloading..."); let a = document.createElement('a'); - a.href = URL.createObjectURL(new Blob([JSON.stringify(pipelineConfig, null, 2)], { - type: 'application/json' - })); + a.href = URL.createObjectURL(new Blob([pipelineConfigJson], { type: 'application/json' })); a.download = pipelineName + '.json'; a.style.display = 'none'; - + document.body.appendChild(a); a.click(); document.body.removeChild(a); } - + + async function processPipelineConfig(configString) { + console.log("configString",configString); let pipelineConfig = JSON.parse(configString); let pipelineList = document.getElementById('pipelineList'); - + while (pipelineList.firstChild) { pipelineList.removeChild(pipelineList.firstChild); } @@ -534,15 +569,15 @@ document.getElementById('addOperationBtn').addEventListener('click', function() let operationsDropdown = document.getElementById('operationsDropdown'); operationsDropdown.value = operationConfig.operation; operationSettings[operationConfig.operation] = operationConfig.parameters; - + // assuming addOperation is async await new Promise((resolve) => { document.getElementById('addOperationBtn').addEventListener('click', resolve, { once: true }); document.getElementById('addOperationBtn').click(); }); - + let lastOperation = pipelineList.lastChild; - + Object.keys(operationConfig.parameters).forEach(parameterName => { let input = document.getElementById(parameterName); if (input) { @@ -559,7 +594,7 @@ document.getElementById('addOperationBtn').addEventListener('click', function() let newInput = document.createElement('input'); newInput.type = 'file'; newInput.id = parameterName; - + // Add the new file input to the main page (change the selector according to your needs) document.querySelector('#main').appendChild(newInput); } @@ -571,15 +606,15 @@ document.getElementById('addOperationBtn').addEventListener('click', function() } } }); - + } } - - + + document.getElementById('uploadPipelineBtn').addEventListener('click', function() { document.getElementById('uploadPipelineInput').click(); }); - + document.getElementById('uploadPipelineInput').addEventListener('change', function(e) { let reader = new FileReader(); reader.onload = function(event) { @@ -588,22 +623,22 @@ document.getElementById('addOperationBtn').addEventListener('click', function() reader.readAsText(e.target.files[0]); hideOrShowPipelineHeader(); }); - + document.getElementById('pipelineSelect').addEventListener('change', function(e) { let selectedPipelineJson = e.target.value; // assuming the selected value is the JSON string of the pipeline config processPipelineConfig(selectedPipelineJson); }); - - - function hideOrShowPipelineHeader() { - var pipelineHeader = document.getElementById('pipelineHeader'); - var pipelineList = document.getElementById('pipelineList'); - if (pipelineList.children.length === 0) { - // Hide the pipeline header if there are no items in the pipeline list - pipelineHeader.style.display = 'none'; - } else { - // Show the pipeline header if there are items in the pipeline list - pipelineHeader.style.display = 'block'; - } + + function hideOrShowPipelineHeader() { + var pipelineHeader = document.getElementById('pipelineHeader'); + var pipelineList = document.getElementById('pipelineList'); + + if (pipelineList.children.length === 0) { + // Hide the pipeline header if there are no items in the pipeline list + pipelineHeader.style.display = 'none'; + } else { + // Show the pipeline header if there are items in the pipeline list + pipelineHeader.style.display = 'block'; + } } diff --git a/src/main/resources/templates/pipeline.html b/src/main/resources/templates/pipeline.html index 51caf186..898976c7 100644 --- a/src/main/resources/templates/pipeline.html +++ b/src/main/resources/templates/pipeline.html @@ -48,6 +48,8 @@
- - - - - - -
-
-
-
-
- -
-
- - - -

Current Limitations

-
    -
  • Cannot have more than one of the same operation
  • -
  • Cannot input additional files via UI
  • -
  • All files and operations run in serial mode
  • -
- -

How it Works Notes

-
    -
  • Configure the pipeline config file and input files to run files against it
  • -
  • For reuse, download the config file and re-upload it when needed, or place it in /pipeline/defaultWebUIConfigs/ to auto-load in the web UI for all users
  • -
- -

How to use pre-load configs in web UI

-
    -
  • Download config files
  • -
  • For reuse, download the config file and re-upload it when needed, or place it in /pipeline/defaultWebUIConfigs/ to auto-load in the web UI for all users
  • -
- -

Todo

-
    -
  • Fix operation adding requering settings to be openned and saved instead of saving defaults
  • -
  • Translation support
  • -
  • offline mode checks and testing
  • -
  • Improve operation config settings UI
  • -
- - -

User Guide for Local Directory Scanning and File Processing

- -

Setting Up Watched Folders:

-

Create a folder where you want your files to be monitored. This is your 'watched folder'.

-

The default directory for this is ./pipeline/watchedFolders/

-

Place any directories you want to be scanned into this folder, this folder should contain multiple folders each for their own tasks and pipelines.

- -

Configuring Processing with JSON Files:

-

In each directory you want processed (e.g ./pipeline/watchedFolders/officePrinter), include a JSON configuration file.

-

This JSON file should specify how you want the files in the directory to be handled (e.g., what operations to perform on them) which can be made, configured and downloaded from Stirling-PDF Pipeline interface.

- -

Automatic Scanning and Processing:

-

The system automatically checks the watched folder every minute for new directories and files to process.

-

When a directory with a valid JSON configuration file is found, it begins processing the files inside as per the configuration.

- -

Processing Steps:

-

Files in each directory are processed according to the instructions in the JSON file.

-

This might involve file conversions, data filtering, renaming files, etc. If the output of a step is a zip, this zip will be automatically unzipped as it passes to next process.

- -

Results and Output:

-

After processing, the results are saved in a specified output location. This could be a different folder or location as defined in the JSON file or the default location ./pipeline/finishedFolders/.

-

Each processed file is named and organized according to the rules set in the JSON configuration.

- -

Completion and Cleanup:

-

Once processing is complete, the original files in the watched folder's directory are removed.

-

You can find the processed files in the designated output location.

- -

Error Handling:

-

If there's an error during processing, the system will not delete the original files, allowing you to check and retry if necessary.

- -

User Interaction:

-

As a user, your main tasks are to set up the watched folders, place directories with files for processing, and create the corresponding JSON configuration files.

-

The system handles the rest, including scanning, processing, and outputting results.

- - - - From 39045df7858d31076530f70f7c58e27101156ede Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sun, 31 Dec 2023 13:34:35 +0000 Subject: [PATCH 20/47] formats --- .../security/SecurityConfiguration.java | 5 +- .../security/UserAuthenticationFilter.java | 20 +- .../api/pipeline/PipelineProcessor.java | 238 +++++++++--------- .../controller/web/GeneralWebController.java | 113 +++++---- .../software/SPDF/utils/RequestUriUtils.java | 19 +- 5 files changed, 201 insertions(+), 194 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java index a2908356..bf1e3661 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -102,8 +102,9 @@ public class SecurityConfiguration { || trimmedUri.startsWith("/images/") || trimmedUri.startsWith("/public/") || trimmedUri.startsWith("/css/") - || trimmedUri.startsWith("/js/") || - trimmedUri.startsWith("/api/v1/info/status"); + || trimmedUri.startsWith("/js/") + || trimmedUri.startsWith( + "/api/v1/info/status"); }) .permitAll() .anyRequest() diff --git a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java index 2a4c68d5..d12fc72b 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java @@ -95,16 +95,16 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { String uri = request.getRequestURI(); String contextPath = request.getContextPath(); String[] permitAllPatterns = { - contextPath + "/login", - contextPath + "/register", - contextPath + "/error", - contextPath + "/images/", - contextPath + "/public/", - contextPath + "/css/", - contextPath + "/js/", - contextPath + "/pdfjs/", - contextPath + "/api/v1/info/status", - contextPath + "/site.webmanifest" + contextPath + "/login", + contextPath + "/register", + contextPath + "/error", + contextPath + "/images/", + contextPath + "/public/", + contextPath + "/css/", + contextPath + "/js/", + contextPath + "/pdfjs/", + contextPath + "/api/v1/info/status", + contextPath + "/site.webmanifest" }; for (String pattern : permitAllPatterns) { diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java index f87cf118..9bebb96f 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java @@ -49,145 +49,153 @@ public class PipelineProcessor { @Autowired(required = false) private UserServiceInterface userService; - @Autowired - private ServletContext servletContext; + @Autowired private ServletContext servletContext; - + private String getApiKeyForUser() { + if (userService == null) return ""; + return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId()); + } + private String getBaseUrl() { + String contextPath = servletContext.getContextPath(); + String port = SPdfApplication.getPort(); - private String getApiKeyForUser() { - if (userService == null) - return ""; - return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId()); - } + return "http://localhost:" + port + contextPath + "/"; + } + List runPipelineAgainstFiles(List outputFiles, PipelineConfig config) + throws Exception { - private String getBaseUrl() { - String contextPath = servletContext.getContextPath(); - String port = SPdfApplication.getPort(); + ByteArrayOutputStream logStream = new ByteArrayOutputStream(); + PrintStream logPrintStream = new PrintStream(logStream); - return "http://localhost:" + port + contextPath + "/"; - } + boolean hasErrors = false; - - - List runPipelineAgainstFiles(List outputFiles, PipelineConfig config) throws Exception { + for (PipelineOperation pipelineOperation : config.getOperations()) { + String operation = pipelineOperation.getOperation(); + boolean isMultiInputOperation = apiDocService.isMultiInput(operation); - ByteArrayOutputStream logStream = new ByteArrayOutputStream(); - PrintStream logPrintStream = new PrintStream(logStream); + logger.info( + "Running operation: {} isMultiInputOperation {}", + operation, + isMultiInputOperation); + Map parameters = pipelineOperation.getParameters(); + String inputFileExtension = ""; - boolean hasErrors = false; + // TODO + // if (operationNode.has("inputFileType")) { + // inputFileExtension = operationNode.get("inputFileType").asText(); + // } else { + inputFileExtension = ".pdf"; + // } + final String finalInputFileExtension = inputFileExtension; - for (PipelineOperation pipelineOperation : config.getOperations()) { - String operation = pipelineOperation.getOperation(); - boolean isMultiInputOperation = apiDocService.isMultiInput(operation); + String url = getBaseUrl() + operation; - logger.info("Running operation: {} isMultiInputOperation {}", operation, isMultiInputOperation); - Map parameters = pipelineOperation.getParameters(); - String inputFileExtension = ""; - - //TODO - //if (operationNode.has("inputFileType")) { - // inputFileExtension = operationNode.get("inputFileType").asText(); - //} else { - inputFileExtension = ".pdf"; - //} - final String finalInputFileExtension = inputFileExtension; - - String url = getBaseUrl() + operation; - - List newOutputFiles = new ArrayList<>(); - if (!isMultiInputOperation) { - for (Resource file : outputFiles) { - boolean hasInputFileType = false; - if (file.getFilename().endsWith(inputFileExtension)) { - hasInputFileType = true; - MultiValueMap body = new LinkedMultiValueMap<>(); - body.add("fileInput", file); + List newOutputFiles = new ArrayList<>(); + if (!isMultiInputOperation) { + for (Resource file : outputFiles) { + boolean hasInputFileType = false; + if (file.getFilename().endsWith(inputFileExtension)) { + hasInputFileType = true; + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("fileInput", file); - - for(Entry entry : parameters.entrySet()) { - body.add(entry.getKey(), entry.getValue()); - } + for (Entry entry : parameters.entrySet()) { + body.add(entry.getKey(), entry.getValue()); + } - ResponseEntity response = sendWebRequest(url, body); + ResponseEntity response = sendWebRequest(url, body); - // If the operation is filter and the response body is null or empty, skip this - // file - if (operation.startsWith("filter-") - && (response.getBody() == null || response.getBody().length == 0)) { - logger.info("Skipping file due to failing {}", operation); - continue; - } + // If the operation is filter and the response body is null or empty, skip + // this + // file + if (operation.startsWith("filter-") + && (response.getBody() == null || response.getBody().length == 0)) { + logger.info("Skipping file due to failing {}", operation); + continue; + } - if (!response.getStatusCode().equals(HttpStatus.OK)) { - logPrintStream.println("Error: " + response.getBody()); - hasErrors = true; - continue; - } - processOutputFiles(operation, file.getFilename(), response, newOutputFiles); - - } + if (!response.getStatusCode().equals(HttpStatus.OK)) { + logPrintStream.println("Error: " + response.getBody()); + hasErrors = true; + continue; + } + processOutputFiles(operation, file.getFilename(), response, newOutputFiles); + } - if (!hasInputFileType) { - logPrintStream.println( - "No files with extension " + inputFileExtension + " found for operation " + operation); - hasErrors = true; - } + if (!hasInputFileType) { + logPrintStream.println( + "No files with extension " + + inputFileExtension + + " found for operation " + + operation); + hasErrors = true; + } + } - - } + } else { + // Filter and collect all files that match the inputFileExtension + List matchingFiles = + outputFiles.stream() + .filter( + file -> + file.getFilename() + .endsWith(finalInputFileExtension)) + .collect(Collectors.toList()); - } else { - // Filter and collect all files that match the inputFileExtension - List matchingFiles = outputFiles.stream() - .filter(file -> file.getFilename().endsWith(finalInputFileExtension)) - .collect(Collectors.toList()); + // Check if there are matching files + if (!matchingFiles.isEmpty()) { + // Create a new MultiValueMap for the request body + MultiValueMap body = new LinkedMultiValueMap<>(); - // Check if there are matching files - if (!matchingFiles.isEmpty()) { - // Create a new MultiValueMap for the request body - MultiValueMap body = new LinkedMultiValueMap<>(); + // Add all matching files to the body + for (Resource file : matchingFiles) { + body.add("fileInput", file); + } - // Add all matching files to the body - for (Resource file : matchingFiles) { - body.add("fileInput", file); - } + for (Entry entry : parameters.entrySet()) { + body.add(entry.getKey(), entry.getValue()); + } - for(Entry entry : parameters.entrySet()) { - body.add(entry.getKey(), entry.getValue()); - } - - ResponseEntity response = sendWebRequest(url, body); + ResponseEntity response = sendWebRequest(url, body); - // Handle the response - if (response.getStatusCode().equals(HttpStatus.OK)) { - processOutputFiles(operation, matchingFiles.get(0).getFilename(), response, newOutputFiles); - } else { - // Log error if the response status is not OK - logPrintStream.println("Error in multi-input operation: " + response.getBody()); - hasErrors = true; - } - } else { - logPrintStream.println("No files with extension " + inputFileExtension + " found for multi-input operation " + operation); - hasErrors = true; - } - } - logPrintStream.close(); - outputFiles = newOutputFiles; + // Handle the response + if (response.getStatusCode().equals(HttpStatus.OK)) { + processOutputFiles( + operation, + matchingFiles.get(0).getFilename(), + response, + newOutputFiles); + } else { + // Log error if the response status is not OK + logPrintStream.println( + "Error in multi-input operation: " + response.getBody()); + hasErrors = true; + } + } else { + logPrintStream.println( + "No files with extension " + + inputFileExtension + + " found for multi-input operation " + + operation); + hasErrors = true; + } + } + logPrintStream.close(); + outputFiles = newOutputFiles; + } + if (hasErrors) { + logger.error("Errors occurred during processing. Log: {}", logStream.toString()); + } - } - if (hasErrors) { - logger.error("Errors occurred during processing. Log: {}", logStream.toString()); - } - - return outputFiles; - } + return outputFiles; + } - private ResponseEntity sendWebRequest(String url, MultiValueMap body ){ - RestTemplate restTemplate = new RestTemplate(); - - // Set up headers, including API key + private ResponseEntity sendWebRequest(String url, MultiValueMap body) { + RestTemplate restTemplate = new RestTemplate(); + + // Set up headers, including API key HttpHeaders headers = new HttpHeaders(); String apiKey = getApiKeyForUser(); 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 ecdbe22d..483053da 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java @@ -33,67 +33,66 @@ import io.swagger.v3.oas.annotations.tags.Tag; @Tag(name = "General", description = "General APIs") public class GeneralWebController { - - @GetMapping("/pipeline") - @Hidden - public String pipelineForm(Model model) { - model.addAttribute("currentPage", "pipeline"); + @GetMapping("/pipeline") + @Hidden + public String pipelineForm(Model model) { + model.addAttribute("currentPage", "pipeline"); - List pipelineConfigs = new ArrayList<>(); - List> pipelineConfigsWithNames = new ArrayList<>(); - - if(new File("./pipeline/defaultWebUIConfigs/").exists()) { - 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); - } - - for (String config : pipelineConfigs) { - Map jsonContent = new ObjectMapper().readValue(config, new TypeReference>(){}); - - String name = (String) jsonContent.get("name"); - if(name == null || name.length() < 1) { - String filename = jsonFiles.get(pipelineConfigs.indexOf(config)).getFileName().toString(); - name = filename.substring(0, filename.lastIndexOf('.')); - } - Map configWithName = new HashMap<>(); - System.out.println("json" + config); - - System.out.println("name" + name); - configWithName.put("json", config); - configWithName.put("name", name); - pipelineConfigsWithNames.add(configWithName); - } - - - - - } catch (IOException e) { - e.printStackTrace(); - } - } - if(pipelineConfigsWithNames.size() == 0) { - Map configWithName = new HashMap<>(); - configWithName.put("json", ""); - configWithName.put("name", "No preloaded configs found"); - pipelineConfigsWithNames.add(configWithName); - } - model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames); + List pipelineConfigs = new ArrayList<>(); + List> pipelineConfigsWithNames = new ArrayList<>(); - model.addAttribute("pipelineConfigs", pipelineConfigs); + if (new File("./pipeline/defaultWebUIConfigs/").exists()) { + try (Stream paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) { + List jsonFiles = + paths.filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith(".json")) + .collect(Collectors.toList()); - return "pipeline"; - } + for (Path jsonFile : jsonFiles) { + String content = Files.readString(jsonFile, StandardCharsets.UTF_8); + pipelineConfigs.add(content); + } + + for (String config : pipelineConfigs) { + Map jsonContent = + new ObjectMapper() + .readValue(config, new TypeReference>() {}); + + String name = (String) jsonContent.get("name"); + if (name == null || name.length() < 1) { + String filename = + jsonFiles + .get(pipelineConfigs.indexOf(config)) + .getFileName() + .toString(); + name = filename.substring(0, filename.lastIndexOf('.')); + } + Map configWithName = new HashMap<>(); + System.out.println("json" + config); + + System.out.println("name" + name); + configWithName.put("json", config); + configWithName.put("name", name); + pipelineConfigsWithNames.add(configWithName); + } + + } catch (IOException e) { + e.printStackTrace(); + } + } + if (pipelineConfigsWithNames.size() == 0) { + Map configWithName = new HashMap<>(); + configWithName.put("json", ""); + configWithName.put("name", "No preloaded configs found"); + pipelineConfigsWithNames.add(configWithName); + } + model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames); + + model.addAttribute("pipelineConfigs", pipelineConfigs); + + return "pipeline"; + } - - - @GetMapping("/merge-pdfs") @Hidden public String mergePdfForm(Model model) { diff --git a/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java b/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java index e52547cf..21324a9c 100644 --- a/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java @@ -2,15 +2,14 @@ package stirling.software.SPDF.utils; public class RequestUriUtils { - public static boolean isStaticResource(String requestURI) { - - return requestURI.startsWith("/css/") - || requestURI.startsWith("/js/") - || requestURI.startsWith("/images/") - || requestURI.startsWith("/public/") - || requestURI.startsWith("/pdfjs/") - || requestURI.endsWith(".svg") - || requestURI.startsWith("/api/v1/info/status"); - + public static boolean isStaticResource(String requestURI) { + + return requestURI.startsWith("/css/") + || requestURI.startsWith("/js/") + || requestURI.startsWith("/images/") + || requestURI.startsWith("/public/") + || requestURI.startsWith("/pdfjs/") + || requestURI.endsWith(".svg") + || requestURI.startsWith("/api/v1/info/status"); } } From 328e87334453da35f1cc25b53c5def36f73a8f32 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sun, 31 Dec 2023 13:35:58 +0000 Subject: [PATCH 21/47] remove prints --- .../software/SPDF/controller/web/GeneralWebController.java | 3 --- 1 file changed, 3 deletions(-) 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 483053da..5615a3a6 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java @@ -68,9 +68,6 @@ public class GeneralWebController { name = filename.substring(0, filename.lastIndexOf('.')); } Map configWithName = new HashMap<>(); - System.out.println("json" + config); - - System.out.println("name" + name); configWithName.put("json", config); configWithName.put("name", name); pipelineConfigsWithNames.add(configWithName); From a5ad9e13fe1edd3d5fcfd53635109ca1b0359990 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sun, 31 Dec 2023 14:56:08 +0000 Subject: [PATCH 22/47] todo --- src/main/resources/templates/pipeline.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/templates/pipeline.html b/src/main/resources/templates/pipeline.html index 2cf9417c..a4af72b3 100644 --- a/src/main/resources/templates/pipeline.html +++ b/src/main/resources/templates/pipeline.html @@ -99,6 +99,7 @@
  • Fix operation adding requering settings to be openned and saved instead of saving defaults
  • Translation support
  • +
  • Save to browser/Account
  • offline mode checks and testing
  • Improve operation config settings UI
  • From b74819cf6c6307561bab568c34007944efa5a204 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sun, 31 Dec 2023 16:01:27 +0000 Subject: [PATCH 23/47] lang --- src/main/resources/messages_en_GB.properties | 22 ++++++++++ src/main/resources/templates/pipeline.html | 44 +++++++++++++------- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 3a21736d..e44aeaf4 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -42,6 +42,7 @@ red=Red green=Green blue=Blue custom=Custom... +WorkInProgess=Work in progress, May not work or be buggy, Please report any ploblems! changedCredsMessage=Credentials changed! notAuthenticatedMessage=User not authenticated. @@ -50,6 +51,27 @@ incorrectPasswordMessage=Current password is incorrect. usernameExistsMessage=New Username already exists. +############### +# Pipeline # +############### +pipeline.header=Pipeline Menu (Alpha) +pipeline.uploadButton=Upload Custom +pipeline.configureButton=Configure +pipeline.defaultOption=Custom +pipeline.submitButton=Submit + +###################### +# Pipeline Options # +###################### +pipelineOptions.header=Pipeline Configuration +pipelineOptions.pipelineNameLabel=Pipeline Name +pipelineOptions.addOperationButton=Add operation +pipelineOptions.pipelineHeader=Pipeline: +pipelineOptions.saveButton=Download +pipelineOptions.validateButton=Validate + + + ############# # NAVBAR # diff --git a/src/main/resources/templates/pipeline.html b/src/main/resources/templates/pipeline.html index a4af72b3..af3581fd 100644 --- a/src/main/resources/templates/pipeline.html +++ b/src/main/resources/templates/pipeline.html @@ -38,13 +38,16 @@
    -

    (Alpha) Pipeline Menu (Huge work in progress, very buggy!)

    +

    +

    - + + data-bs-toggle="modal" data-bs-target="#pipelineSettingsModal" + th:text="#{pipeline.configureButton}"> +
    @@ -52,7 +55,8 @@ +
    - + +
    - + +
    @@ -214,8 +223,11 @@