From f92482d89e4e7f719b1f0b90b89ac1e69c08f924 Mon Sep 17 00:00:00 2001
From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Date: Tue, 4 Jul 2023 21:45:35 +0100
Subject: [PATCH 01/26] page numbers and custom images
---
.gitignore | 6 +
Dockerfile-lite | 6 +
README.md | 6 +-
.../software/SPDF/SPdfApplication.java | 10 +
.../software/SPDF/config/WebMvcConfig.java | 9 +
.../controller/api/ScalePagesController.java | 4 +-
.../api/other/CompressController.java | 9 +
.../api/other/PageNumbersController.java | 176 ++++++++++++++++++
.../controller/web/OtherWebController.java | 7 +
.../software/SPDF/utils/GeneralUtils.java | 16 ++
src/main/resources/application.properties | 6 +-
src/main/resources/messages_en_GB.properties | 7 +-
.../static/images/add-page-numbers.svg | 3 +
.../resources/templates/fragments/navbar.html | 3 +-
src/main/resources/templates/home.html | 6 +-
.../templates/other/add-page-numbers.html | 51 +++++
16 files changed, 317 insertions(+), 8 deletions(-)
create mode 100644 src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java
create mode 100644 src/main/resources/static/images/add-page-numbers.svg
create mode 100644 src/main/resources/templates/other/add-page-numbers.html
diff --git a/.gitignore b/.gitignore
index 0a5cd198..ff36fc93 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,12 @@ local.properties
.project
version.properties
+#### Stirling-PDF Files ###
+customFiles/
+config/
+watchedFolders/
+
+
# Gradle
.gradle
.lock
diff --git a/Dockerfile-lite b/Dockerfile-lite
index d3968a2a..eb92e487 100644
--- a/Dockerfile-lite
+++ b/Dockerfile-lite
@@ -10,6 +10,12 @@ RUN apt-get update && \
unoconv && \
rm -rf /var/lib/apt/lists/*
+#Install fonts
+RUN mkdir /usr/share/fonts/opentype/noto/
+COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
+COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
+RUN fc-cache -f -v
+
# Copy the application JAR file
COPY build/libs/*.jar app.jar
diff --git a/README.md b/README.md
index 0398bf23..b2606c7d 100644
--- a/README.md
+++ b/README.md
@@ -86,6 +86,8 @@ docker run -d \
Can also add these for customisation but are not required
+ -v /location/of/extraConfigs:/configs \
+ -v /location/of/customFiles:/customFiles \
-e APP_HOME_NAME="Stirling PDF" \
-e APP_HOME_DESCRIPTION="Your locally hosted one-stop-shop for all your PDF needs." \
-e APP_NAVBAR_NAME="Stirling PDF" \
@@ -104,6 +106,7 @@ services:
volumes:
- /location/of/trainingData:/usr/share/tesseract-ocr/4.00/tessdata #Required for extra OCR languages
# - /location/of/extraConfigs:/configs
+# - /location/of/customFiles:/customFiles/
# environment:
# APP_LOCALE: en_GB
# APP_HOME_NAME: Stirling PDF
@@ -161,10 +164,11 @@ Using the same method you can also change
- Change root URI for Stirling-PDF ie change server.com/ to server.com/pdf-app by running APP_ROOT_PATH as pdf-app
- Disable and remove endpoints and functionality from Stirling-PDF. Currently the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma seperated lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image to pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Frooodle/Stirling-PDF/blob/main/groups.md)
- Change the max file size allowed through the server with the environment variable MAX_FILE_SIZE. default 2000MB
+- Customise static files such as app logo by placing files in the /customFiles/static/ directory. Example to customise app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF
## API
For those wanting to use Stirling-PDFs backend API to link with their own custom scripting to edit PDFs you can view all existing API documentation
-[here](https://app.swaggerhub.com/apis-docs/Frooodle/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation
+[here](https://app.swaggerhub.com/apis-docs/Frooodle/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF)
## FAQ
diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java
index 02f66c22..6a953e49 100644
--- a/src/main/java/stirling/software/SPDF/SPdfApplication.java
+++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java
@@ -1,5 +1,10 @@
package stirling.software.SPDF;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@@ -7,6 +12,7 @@ import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.EnableScheduling;
import jakarta.annotation.PostConstruct;
+import stirling.software.SPDF.utils.GeneralUtils;
@SpringBootApplication
//@EnableScheduling
@@ -49,6 +55,10 @@ public class SPdfApplication {
// TODO Auto-generated catch block
e.printStackTrace();
}
+
+ GeneralUtils.createDir("customFiles/static/");
+ GeneralUtils.createDir("customFiles/templates/");
+ GeneralUtils.createDir("config");
System.out.println("Stirling-PDF Started.");
String port = System.getProperty("local.server.port");
diff --git a/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java b/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java
index 610b11c0..10a88e97 100644
--- a/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java
+++ b/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java
@@ -3,6 +3,7 @@ package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@@ -15,4 +16,12 @@ public class WebMvcConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(endpointInterceptor);
}
+
+ @Override
+ public void addResourceHandlers(ResourceHandlerRegistry registry) {
+ // Handler for external static resources
+ registry.addResourceHandler("/**")
+ .addResourceLocations("file:customFiles/static/", "classpath:/static/")
+ .setCachePeriod(0); // Optional: disable caching
+ }
}
diff --git a/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java b/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java
index 420902cf..e3b4434a 100644
--- a/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java
+++ b/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java
@@ -52,11 +52,11 @@ public class ScalePagesController {
@Operation(summary = "Change the size of a PDF page/document", description = "This operation takes an input PDF file and the size to scale the pages to in the output PDF file. Input:PDF Output:PDF Type:SISO")
public ResponseEntity scalePages(
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
- @Parameter(description = "The scale of pages in the output PDF. Acceptable values are A0-A10, B0-B9, LETTER, TABLOID, LEDGER, LEGAL, EXECUTIVE.", required = true, schema = @Schema(type = "String", allowableValues = {
+ @Parameter(description = "The scale of pages in the output PDF. Acceptable values are A0-A10, B0-B9, LETTER, TABLOID, LEDGER, LEGAL, EXECUTIVE.", required = true, schema = @Schema(type = "string", allowableValues = {
"A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10", "B0", "B1", "B2", "B3", "B4",
"B5", "B6", "B7", "B8", "B9", "LETTER", "TABLOID", "LEDGER", "LEGAL",
"EXECUTIVE" })) @RequestParam("pageSize") String targetPageSize,
- @Parameter(description = "The scale of the content on the pages of the output PDF. Acceptable values are floats.", required = true, schema = @Schema(type = "float")) @RequestParam("scaleFactor") float scaleFactor)
+ @Parameter(description = "The scale of the content on the pages of the output PDF. Acceptable values are floats.", required = true, schema = @Schema(type = "integer")) @RequestParam("scaleFactor") float scaleFactor)
throws IOException {
Map sizeMap = new HashMap<>();
diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/CompressController.java b/src/main/java/stirling/software/SPDF/controller/api/other/CompressController.java
index 0002b808..42ab6a41 100644
--- a/src/main/java/stirling/software/SPDF/controller/api/other/CompressController.java
+++ b/src/main/java/stirling/software/SPDF/controller/api/other/CompressController.java
@@ -221,6 +221,15 @@ public class CompressController {
// Read the optimized PDF file
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
+ // Check if optimized file is larger than the original
+ if(pdfBytes.length > inputFileSize) {
+ // Log the occurrence
+ logger.warn("Optimized file is larger than the original. Returning the original file instead.");
+
+ // Read the original file again
+ pdfBytes = Files.readAllBytes(tempInputFile);
+ }
+
// Clean up the temporary files
Files.delete(tempInputFile);
Files.delete(tempOutputFile);
diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java b/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java
new file mode 100644
index 00000000..ab27d9a0
--- /dev/null
+++ b/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java
@@ -0,0 +1,176 @@
+package stirling.software.SPDF.controller.api.other;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RequestPart;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import stirling.software.SPDF.utils.GeneralUtils;
+import stirling.software.SPDF.utils.PdfUtils;
+import stirling.software.SPDF.utils.WebResponseUtils;
+import org.apache.pdfbox.pdmodel.*;
+import org.apache.pdfbox.pdmodel.common.*;
+import org.apache.pdfbox.pdmodel.PDPageContentStream.*;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.http.*;
+import org.springframework.web.multipart.MultipartFile;
+import io.swagger.v3.oas.annotations.*;
+import io.swagger.v3.oas.annotations.media.*;
+import io.swagger.v3.oas.annotations.parameters.*;
+import org.apache.pdfbox.pdmodel.font.PDType1Font;
+import org.apache.tomcat.util.http.ResponseUtil;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.List;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.multipart.MultipartFile;
+
+import com.itextpdf.io.font.constants.StandardFonts;
+import com.itextpdf.kernel.font.PdfFont;
+import com.itextpdf.kernel.font.PdfFontFactory;
+import com.itextpdf.kernel.geom.Rectangle;
+import com.itextpdf.kernel.pdf.PdfReader;
+import com.itextpdf.kernel.pdf.PdfWriter;
+import com.itextpdf.kernel.pdf.PdfDocument;
+import com.itextpdf.kernel.pdf.PdfPage;
+import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
+import com.itextpdf.layout.Canvas;
+import com.itextpdf.layout.element.Paragraph;
+import com.itextpdf.layout.properties.TextAlignment;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.io.*;
+
+@RestController
+@Tag(name = "Other", description = "Other APIs")
+public class PageNumbersController {
+
+ private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class);
+
+ @PostMapping(value = "/add-page-numbers", consumes = "multipart/form-data")
+ @Operation(summary = "Add page numbers to a PDF document", description = "This operation takes an input PDF file and adds page numbers to it. Input:PDF Output:PDF Type:SISO")
+ public ResponseEntity addPageNumbers(
+ @Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
+ @Parameter(description = "Custom margin: small/medium/large", required = true, schema = @Schema(type = "string", allowableValues = {"small", "medium", "large"})) @RequestParam("customMargin") String customMargin,
+ @Parameter(description = "Position: 1 of 9 positions", required = true, schema = @Schema(type = "integer", minimum = "1", maximum = "9")) @RequestParam("position") int position,
+ @Parameter(description = "Starting number", required = true, schema = @Schema(type = "integer", minimum = "1")) @RequestParam("startingNumber") int startingNumber,
+ @Parameter(description = "Which pages to number, default all", required = false, schema = @Schema(type = "string")) @RequestParam(value = "pagesToNumber", required = false) String pagesToNumber,
+ @Parameter(description = "Custom text: defaults to just number but can have things like \"Page {n} of {p}\"", required = false, schema = @Schema(type = "string")) @RequestParam(value = "customText", required = false) String customText)
+ throws IOException {
+
+ byte[] fileBytes = file.getBytes();
+ ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
+
+ int pageNumber = startingNumber;
+ float marginFactor;
+ switch (customMargin.toLowerCase()) {
+ case "small":
+ marginFactor = 0.01f;
+ break;
+ case "medium":
+ marginFactor = 0.025f;
+ break;
+ case "large":
+ marginFactor = 0.05f;
+ break;
+ case "x-large":
+ marginFactor = 0.1f;
+ break;
+ default:
+ marginFactor = 0.01f;
+ break;
+ }
+
+ float fontSize = 12.0f;
+
+ PdfReader reader = new PdfReader(bais);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ PdfWriter writer = new PdfWriter(baos);
+
+ PdfDocument pdfDoc = new PdfDocument(reader, writer);
+
+ List pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), pdfDoc.getNumberOfPages());
+
+ for (int i : pagesToNumberList) {
+ PdfPage page = pdfDoc.getPage(i+1);
+ Rectangle pageSize = page.getPageSize();
+ PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamBefore(), page.getResources(), pdfDoc);
+
+ String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{p}", String.valueOf(pdfDoc.getNumberOfPages())) : String.valueOf(pageNumber);
+
+ PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA);
+ float textWidth = font.getWidth(text, fontSize);
+ float textHeight = font.getAscent(text, fontSize) - font.getDescent(text, fontSize);
+
+ float x, y;
+ TextAlignment alignment;
+
+ int xGroup = (position - 1) % 3;
+ int yGroup = 2 - (position - 1) / 3;
+
+ switch (xGroup) {
+ case 0: // left
+ x = pageSize.getLeft() + marginFactor * pageSize.getWidth();
+ alignment = TextAlignment.LEFT;
+ break;
+ case 1: // center
+ x = pageSize.getLeft() + (pageSize.getWidth()) / 2;
+ alignment = TextAlignment.CENTER;
+ break;
+ default: // right
+ x = pageSize.getRight() - marginFactor * pageSize.getWidth();
+ alignment = TextAlignment.RIGHT;
+ break;
+ }
+
+ switch (yGroup) {
+ case 0: // bottom
+ y = pageSize.getBottom() + marginFactor * pageSize.getHeight();
+ break;
+ case 1: // middle
+ y = pageSize.getBottom() + (pageSize.getHeight() ) / 2;
+ break;
+ default: // top
+ y = pageSize.getTop() - marginFactor * pageSize.getHeight();
+ break;
+ }
+
+ new Canvas(pdfCanvas, page.getPageSize())
+ .showTextAligned(new Paragraph(text).setFont(font).setFontSize(fontSize), x, y, alignment);
+
+ pageNumber++;
+ }
+
+
+ pdfDoc.close();
+ byte[] resultBytes = baos.toByteArray();
+
+ return ResponseEntity.ok()
+ .header("Content-Type", "application/pdf; charset=UTF-8")
+ .header("Content-Disposition", "inline; filename=" + URLEncoder.encode(file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", "UTF-8"))
+ .body(resultBytes);
+ }
+
+
+
+}
diff --git a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java
index 2c11decc..2510f18a 100644
--- a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java
+++ b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java
@@ -32,6 +32,13 @@ public class OtherWebController {
return modelAndView;
}
+ @GetMapping("/add-page-numbers")
+ @Hidden
+ public String addPageNumbersForm(Model model) {
+ model.addAttribute("currentPage", "add-page-numbers");
+ return "other/add-page-numbers";
+ }
+
@GetMapping("/extract-images")
@Hidden
public String extractImagesForm(Model model) {
diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java
index 1e101d09..c2e5aaf6 100644
--- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java
+++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java
@@ -1,5 +1,9 @@
package stirling.software.SPDF.utils;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
@@ -88,4 +92,16 @@ public class GeneralUtils {
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/resources/application.properties b/src/main/resources/application.properties
index 6d5dfe95..06e4edec 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -15,7 +15,7 @@ server.error.whitelabel.enabled=false
server.error.include-stacktrace=always
server.error.include-exception=true
server.error.include-message=always
-
+\
server.servlet.session.tracking-modes=cookie
server.servlet.context-path=${APP_ROOT_PATH:/}
@@ -26,3 +26,7 @@ spring.thymeleaf.encoding=UTF-8
server.connection-timeout=${CONNECTION_TIMEOUT:5m}
spring.mvc.async.request-timeout=${ASYNC_CONNECTION_TIMEOUT:300000}
+
+spring.resources.static-locations=file:customFiles/static/
+#spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/
+#spring.thymeleaf.cache=false
\ No newline at end of file
diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties
index b224a758..37123c81 100644
--- a/src/main/resources/messages_en_GB.properties
+++ b/src/main/resources/messages_en_GB.properties
@@ -132,8 +132,11 @@ home.pageLayout.desc=Merge multiple pages of a PDF document into a single page
home.scalePages.title=Adjust page size/scale
home.scalePages.desc=Change the size/scale of a page and/or its contents.
-home.pipeline.title=Pipeline
-home.pipeline.desc=Pipeline desc.
+home.pipeline.title=Pipeline (Advanced)
+home.pipeline.desc=Run multiple actions on PDFs by defining pipeline scripts
+
+home.add-page-numbers.title=Add Page Numbers
+home.add-page-numbers.desc=Add Page numbers throughout a document in a set location
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect
diff --git a/src/main/resources/static/images/add-page-numbers.svg b/src/main/resources/static/images/add-page-numbers.svg
new file mode 100644
index 00000000..3ee3396c
--- /dev/null
+++ b/src/main/resources/static/images/add-page-numbers.svg
@@ -0,0 +1,3 @@
+
\ 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 db6308e3..09b60f33 100644
--- a/src/main/resources/templates/fragments/navbar.html
+++ b/src/main/resources/templates/fragments/navbar.html
@@ -13,7 +13,7 @@
-
+
@@ -116,6 +116,7 @@
+
diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html
index 47fbb2f2..f7e0177c 100644
--- a/src/main/resources/templates/home.html
+++ b/src/main/resources/templates/home.html
@@ -22,7 +22,7 @@
-
+
@@ -67,6 +67,10 @@
+
+
+
+
diff --git a/src/main/resources/templates/other/add-page-numbers.html b/src/main/resources/templates/other/add-page-numbers.html
new file mode 100644
index 00000000..59cd0403
--- /dev/null
+++ b/src/main/resources/templates/other/add-page-numbers.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From 4e28bf03bdbd778a93004fe40516f65f671b1543 Mon Sep 17 00:00:00 2001
From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Date: Tue, 4 Jul 2023 23:25:21 +0100
Subject: [PATCH 02/26] auto rename
---
.../api/other/AutoRenameController.java | 164 ++++++++++++++++++
.../controller/web/OtherWebController.java | 7 +
src/main/resources/messages_en_GB.properties | 4 +
src/main/resources/static/images/fonts.svg | 3 +
.../resources/templates/fragments/navbar.html | 3 +-
src/main/resources/templates/home.html | 2 +-
.../templates/other/auto-rename.html | 31 ++++
7 files changed, 212 insertions(+), 2 deletions(-)
create mode 100644 src/main/java/stirling/software/SPDF/controller/api/other/AutoRenameController.java
create mode 100644 src/main/resources/static/images/fonts.svg
create mode 100644 src/main/resources/templates/other/auto-rename.html
diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/AutoRenameController.java b/src/main/java/stirling/software/SPDF/controller/api/other/AutoRenameController.java
new file mode 100644
index 00000000..bfc7674e
--- /dev/null
+++ b/src/main/java/stirling/software/SPDF/controller/api/other/AutoRenameController.java
@@ -0,0 +1,164 @@
+package stirling.software.SPDF.controller.api.other;
+
+import java.io.IOException;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RequestPart;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import stirling.software.SPDF.utils.GeneralUtils;
+import stirling.software.SPDF.utils.PdfUtils;
+import stirling.software.SPDF.utils.WebResponseUtils;
+import org.apache.pdfbox.pdmodel.*;
+import org.apache.pdfbox.pdmodel.common.*;
+import org.apache.pdfbox.pdmodel.PDPageContentStream.*;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.http.*;
+import org.springframework.web.multipart.MultipartFile;
+import io.swagger.v3.oas.annotations.*;
+import io.swagger.v3.oas.annotations.media.*;
+import io.swagger.v3.oas.annotations.parameters.*;
+import org.apache.pdfbox.pdmodel.font.PDType1Font;
+import org.apache.pdfbox.text.TextPosition;
+import org.apache.tomcat.util.http.ResponseUtil;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.List;
+import java.util.ArrayList;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.multipart.MultipartFile;
+
+import com.itextpdf.io.font.constants.StandardFonts;
+import com.itextpdf.kernel.font.PdfFont;
+import com.itextpdf.kernel.font.PdfFontFactory;
+import com.itextpdf.kernel.geom.Rectangle;
+import com.itextpdf.kernel.pdf.PdfReader;
+import com.itextpdf.kernel.pdf.PdfWriter;
+import com.itextpdf.kernel.pdf.PdfDocument;
+import com.itextpdf.kernel.pdf.PdfPage;
+import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
+import com.itextpdf.layout.Canvas;
+import com.itextpdf.layout.element.Paragraph;
+import com.itextpdf.layout.properties.TextAlignment;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.io.*;
+import org.apache.pdfbox.pdmodel.*;
+import org.apache.pdfbox.text.*;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import io.swagger.v3.oas.annotations.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.springframework.http.ResponseEntity;
+@RestController
+@Tag(name = "Other", description = "Other APIs")
+public class AutoRenameController {
+
+ private static final Logger logger = LoggerFactory.getLogger(AutoRenameController.class);
+
+ private static final float TITLE_FONT_SIZE_THRESHOLD = 20.0f;
+ private static final int LINE_LIMIT = 7;
+
+ @PostMapping(consumes = "multipart/form-data", value = "/auto-rename")
+ @Operation(summary = "Extract header from PDF file", description = "This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO")
+ public ResponseEntity
extractHeader(
+ @RequestPart(value = "fileInput") @Parameter(description = "The input PDF file from which the header is to be extracted.", required = true) MultipartFile file,
+ @RequestParam(required = false, defaultValue = "false") @Parameter(description = "Flag indicating whether to use the first text as a fallback if no suitable title is found. Defaults to false.", required = false) Boolean useFirstTextAsFallback)
+ throws Exception {
+
+ PDDocument document = PDDocument.load(file.getInputStream());
+ PDFTextStripper reader = new PDFTextStripper() {
+ class LineInfo {
+ String text;
+ float fontSize;
+
+ LineInfo(String text, float fontSize) {
+ this.text = text;
+ this.fontSize = fontSize;
+ }
+ }
+
+ List lineInfos = new ArrayList<>();
+ StringBuilder lineBuilder = new StringBuilder();
+ float lastY = -1;
+ float maxFontSizeInLine = 0.0f;
+ int lineCount = 0;
+
+ @Override
+ protected void processTextPosition(TextPosition text) {
+ if (lastY != text.getY() && lineCount < LINE_LIMIT) {
+ processLine();
+ lineBuilder = new StringBuilder(text.getUnicode());
+ maxFontSizeInLine = text.getFontSizeInPt();
+ lastY = text.getY();
+ lineCount++;
+ } else if (lineCount < LINE_LIMIT) {
+ lineBuilder.append(text.getUnicode());
+ if (text.getFontSizeInPt() > maxFontSizeInLine) {
+ maxFontSizeInLine = text.getFontSizeInPt();
+ }
+ }
+ }
+
+ private void processLine() {
+ if (lineBuilder.length() > 0 && lineCount < LINE_LIMIT) {
+ lineInfos.add(new LineInfo(lineBuilder.toString(), maxFontSizeInLine));
+ }
+ }
+
+ @Override
+ public String getText(PDDocument doc) throws IOException {
+ this.lineInfos.clear();
+ this.lineBuilder = new StringBuilder();
+ this.lastY = -1;
+ this.maxFontSizeInLine = 0.0f;
+ this.lineCount = 0;
+ super.getText(doc);
+ processLine(); // Process the last line
+
+ // Sort lines by font size in descending order and get the first one
+ lineInfos.sort(Comparator.comparing((LineInfo li) -> li.fontSize).reversed());
+ String title = lineInfos.isEmpty() ? null : lineInfos.get(0).text;
+
+ return title != null ? title : (useFirstTextAsFallback ? (lineInfos.isEmpty() ? null : lineInfos.get(lineInfos.size() - 1).text) : null);
+ }
+ };
+
+ String header = reader.getText(document);
+
+
+
+ // Sanitize the header string by removing characters not allowed in a filename.
+ if (header != null && header.length() < 255) {
+ header = header.replaceAll("[/\\\\?%*:|\"<>]", "");
+ return WebResponseUtils.pdfDocToWebResponse(document, header + ".pdf");
+ } else {
+ logger.info("File has no good title to be found");
+ return WebResponseUtils.pdfDocToWebResponse(document, file.getOriginalFilename());
+ }
+ }
+
+
+
+
+}
diff --git a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java
index 2510f18a..e3c7a41b 100644
--- a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java
+++ b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java
@@ -140,4 +140,11 @@ public class OtherWebController {
return "other/auto-crop";
}
+ @GetMapping("/auto-rename")
+ @Hidden
+ public String autoRenameForm(Model model) {
+ model.addAttribute("currentPage", "auto-rename");
+ return "other/auto-rename";
+ }
+
}
diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties
index 37123c81..f28286ba 100644
--- a/src/main/resources/messages_en_GB.properties
+++ b/src/main/resources/messages_en_GB.properties
@@ -138,6 +138,10 @@ home.pipeline.desc=Run multiple actions on PDFs by defining pipeline scripts
home.add-page-numbers.title=Add Page Numbers
home.add-page-numbers.desc=Add Page numbers throughout a document in a set location
+home.auto-rename.title=Auto Rename PDF File
+home.auto-rename.desc=Auto renames a PDF file based on its detected header
+
+
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect
downloadPdf=Download PDF
diff --git a/src/main/resources/static/images/fonts.svg b/src/main/resources/static/images/fonts.svg
new file mode 100644
index 00000000..3afc7d2e
--- /dev/null
+++ b/src/main/resources/static/images/fonts.svg
@@ -0,0 +1,3 @@
+
\ 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 09b60f33..9403af42 100644
--- a/src/main/resources/templates/fragments/navbar.html
+++ b/src/main/resources/templates/fragments/navbar.html
@@ -116,7 +116,8 @@
-
+
+
diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html
index f7e0177c..33bf6b42 100644
--- a/src/main/resources/templates/home.html
+++ b/src/main/resources/templates/home.html
@@ -69,7 +69,7 @@
-
+
diff --git a/src/main/resources/templates/other/auto-rename.html b/src/main/resources/templates/other/auto-rename.html
new file mode 100644
index 00000000..70641684
--- /dev/null
+++ b/src/main/resources/templates/other/auto-rename.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From a3c7f5aa4662798aedd9873d5f295617f47ae114 Mon Sep 17 00:00:00 2001
From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Date: Wed, 5 Jul 2023 22:21:43 +0100
Subject: [PATCH 03/26] Search bar and adjust contrast
---
.../api/other/AutoRenameController.java | 23 +-
.../controller/web/OtherWebController.java | 2 +
src/main/resources/messages_en_GB.properties | 2 +
src/main/resources/static/css/home.css | 14 ++
src/main/resources/static/css/navbar.css | 38 +++
.../static/images/adjust-contrast.svg | 4 +
src/main/resources/static/js/homecard.js | 24 ++
src/main/resources/static/js/search.js | 72 ++++++
.../resources/templates/fragments/card.html | 2 +-
.../resources/templates/fragments/navbar.html | 18 ++
src/main/resources/templates/home.html | 15 +-
.../templates/other/adjust-contrast.html | 216 +++++++++++++++++-
12 files changed, 411 insertions(+), 19 deletions(-)
create mode 100644 src/main/resources/static/images/adjust-contrast.svg
create mode 100644 src/main/resources/static/js/search.js
diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/AutoRenameController.java b/src/main/java/stirling/software/SPDF/controller/api/other/AutoRenameController.java
index bfc7674e..66fd70a6 100644
--- a/src/main/java/stirling/software/SPDF/controller/api/other/AutoRenameController.java
+++ b/src/main/java/stirling/software/SPDF/controller/api/other/AutoRenameController.java
@@ -77,7 +77,7 @@ public class AutoRenameController {
private static final Logger logger = LoggerFactory.getLogger(AutoRenameController.class);
private static final float TITLE_FONT_SIZE_THRESHOLD = 20.0f;
- private static final int LINE_LIMIT = 7;
+ 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")
@@ -136,12 +136,25 @@ public class AutoRenameController {
super.getText(doc);
processLine(); // Process the last line
- // Sort lines by font size in descending order and get the first one
- lineInfos.sort(Comparator.comparing((LineInfo li) -> li.fontSize).reversed());
- String title = lineInfos.isEmpty() ? null : lineInfos.get(0).text;
+ // 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));
+ }
- return title != null ? title : (useFirstTextAsFallback ? (lineInfos.isEmpty() ? null : lineInfos.get(lineInfos.size() - 1).text) : null);
+ // Sort lines by font size in descending order and get the first one
+ mergedLineInfos.sort(Comparator.comparing((LineInfo li) -> li.fontSize).reversed());
+ String title = mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(0).text;
+
+ return title != null ? title : (useFirstTextAsFallback ? (mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(mergedLineInfos.size() - 1).text) : null);
}
+
};
String header = reader.getText(document);
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 e3c7a41b..8fa57d08 100644
--- a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java
+++ b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java
@@ -147,4 +147,6 @@ public class OtherWebController {
return "other/auto-rename";
}
+
+
}
diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties
index f28286ba..15ff69d3 100644
--- a/src/main/resources/messages_en_GB.properties
+++ b/src/main/resources/messages_en_GB.properties
@@ -35,9 +35,11 @@ navbar.pageOps=Page Operations
home.multiTool.title=PDF Multi Tool
home.multiTool.desc=Merge, Rotate, Rearrange, and Remove pages
+multiTool.tags=Multi Tool,Multi operation,UI,click drag,front end,client side
home.merge.title=Merge
home.merge.desc=Easily merge multiple PDFs into one.
+merge.tags=merge,Page operations,Back end,server side
home.split.title=Split
home.split.desc=Split PDFs into multiple documents
diff --git a/src/main/resources/static/css/home.css b/src/main/resources/static/css/home.css
index 94414be9..998278e1 100644
--- a/src/main/resources/static/css/home.css
+++ b/src/main/resources/static/css/home.css
@@ -1,3 +1,17 @@
+#searchBar {
+ background-image: url('/images/search.svg');
+ background-position: 16px 16px;
+ background-repeat: no-repeat;
+ width: 100%;
+ font-size: 16px;
+ margin-bottom: 12px;
+ padding: 12px 20px 12px 40px;
+ border: 1px solid #ddd;
+
+
+}
+
+
.features-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(21rem, 3fr));
diff --git a/src/main/resources/static/css/navbar.css b/src/main/resources/static/css/navbar.css
index 60b612c5..5bb99a5e 100644
--- a/src/main/resources/static/css/navbar.css
+++ b/src/main/resources/static/css/navbar.css
@@ -1,3 +1,41 @@
+
+
+#navbarSearch {
+ top: 100%;
+ right: 0;
+}
+
+#searchForm {
+ width: 200px; /* Adjust this value as needed */
+}
+
+/* Style the search results to match the navbar */
+#searchResults {
+ max-height: 200px; /* Adjust this value as needed */
+ overflow-y: auto;
+ width: 100%;
+}
+
+#searchResults .dropdown-item {
+ display: flex;
+ align-items: center;
+ white-space: nowrap;
+ height: 50px; /* Fixed height */
+ overflow: hidden; /* Hide overflow */
+}
+
+#searchResults .icon {
+ margin-right: 10px;
+}
+
+#searchResults .icon-text {
+ display: inline;
+ overflow: hidden; /* Hide overflow */
+ text-overflow: ellipsis; /* Add ellipsis for long text */
+}
+
+
+
.main-icon {
width: 36px;
height: 36px;
diff --git a/src/main/resources/static/images/adjust-contrast.svg b/src/main/resources/static/images/adjust-contrast.svg
new file mode 100644
index 00000000..fea76d92
--- /dev/null
+++ b/src/main/resources/static/images/adjust-contrast.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/src/main/resources/static/js/homecard.js b/src/main/resources/static/js/homecard.js
index f3d3de35..fb962a12 100644
--- a/src/main/resources/static/js/homecard.js
+++ b/src/main/resources/static/js/homecard.js
@@ -1,3 +1,24 @@
+function filterCards() {
+ var input = document.getElementById('searchBar');
+ var filter = input.value.toUpperCase();
+ var cards = document.querySelectorAll('.feature-card');
+
+ for (var i = 0; i < cards.length; i++) {
+ var card = cards[i];
+ var title = card.querySelector('h5.card-title').innerText;
+ var text = card.querySelector('p.card-text').innerText;
+ var tags = card.getAttribute('data-tags');
+ var content = title + ' ' + text + ' ' + tags;
+
+ if (content.toUpperCase().indexOf(filter) > -1) {
+ card.style.display = "";
+ } else {
+ card.style.display = "none";
+ }
+ }
+}
+
+
function toggleFavorite(element) {
var img = element.querySelector('img');
var card = element.closest('.feature-card');
@@ -13,6 +34,7 @@ function toggleFavorite(element) {
}
reorderCards();
updateFavoritesDropdown();
+ filterCards();
}
function reorderCards() {
@@ -45,5 +67,7 @@ function initializeCards() {
});
reorderCards();
updateFavoritesDropdown();
+ filterCards();
}
+
window.onload = initializeCards;
\ No newline at end of file
diff --git a/src/main/resources/static/js/search.js b/src/main/resources/static/js/search.js
new file mode 100644
index 00000000..3c19ed84
--- /dev/null
+++ b/src/main/resources/static/js/search.js
@@ -0,0 +1,72 @@
+// Toggle search bar when the search icon is clicked
+document.querySelector('#search-icon').addEventListener('click', function(e) {
+ e.preventDefault();
+ var searchBar = document.querySelector('#navbarSearch');
+ searchBar.classList.toggle('show');
+});
+window.onload = function() {
+ var items = document.querySelectorAll('.dropdown-item, .nav-link');
+ var dummyContainer = document.createElement('div');
+ dummyContainer.style.position = 'absolute';
+ dummyContainer.style.visibility = 'hidden';
+ dummyContainer.style.whiteSpace = 'nowrap'; // Ensure we measure full width
+ document.body.appendChild(dummyContainer);
+
+ var maxWidth = 0;
+
+ items.forEach(function(item) {
+ var clone = item.cloneNode(true);
+ dummyContainer.appendChild(clone);
+ var width = clone.offsetWidth;
+ if (width > maxWidth) {
+ maxWidth = width;
+ }
+ dummyContainer.removeChild(clone);
+ });
+
+ document.body.removeChild(dummyContainer);
+
+ // Store max width for later use
+ window.navItemMaxWidth = maxWidth;
+};
+
+// Show search results as user types in search box
+document.querySelector('#navbarSearchInput').addEventListener('input', function(e) {
+ var searchText = e.target.value.toLowerCase();
+ var items = document.querySelectorAll('.dropdown-item, .nav-link');
+ var resultsBox = document.querySelector('#searchResults');
+
+ // Clear any previous results
+ resultsBox.innerHTML = '';
+
+ items.forEach(function(item) {
+ var titleElement = item.querySelector('.icon-text');
+ var iconElement = item.querySelector('.icon');
+ var itemHref = item.getAttribute('href');
+ if (titleElement && iconElement && itemHref !== '#') {
+ var title = titleElement.innerText.toLowerCase();
+ if (title.indexOf(searchText) !== -1 && !resultsBox.querySelector(`a[href="${item.getAttribute('href')}"]`)) {
+ var result = document.createElement('a');
+ result.href = itemHref;
+ result.classList.add('dropdown-item');
+
+ var resultIcon = document.createElement('img');
+ resultIcon.src = iconElement.src;
+ resultIcon.alt = 'icon';
+ resultIcon.classList.add('icon');
+ result.appendChild(resultIcon);
+
+ var resultText = document.createElement('span');
+ resultText.textContent = title;
+ resultText.classList.add('icon-text');
+ result.appendChild(resultText);
+
+ resultsBox.appendChild(result);
+ }
+ }
+ });
+
+ // Set the width of the search results box to the maximum width
+ resultsBox.style.width = window.navItemMaxWidth + 'px';
+});
+
diff --git a/src/main/resources/templates/fragments/card.html b/src/main/resources/templates/fragments/card.html
index faa816a4..48142b03 100644
--- a/src/main/resources/templates/fragments/card.html
+++ b/src/main/resources/templates/fragments/card.html
@@ -1,4 +1,4 @@
-
+
-
-
+
+
+
+
+
+
+
@@ -70,13 +75,13 @@
-
+
-
+
-
+
-
\ No newline at end of file
From 6e726ac2a66008e7ad5f4041c2c031a8c7f05830 Mon Sep 17 00:00:00 2001
From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Date: Sun, 9 Jul 2023 00:05:33 +0100
Subject: [PATCH 05/26] lots of stuff and garbage code for automate to cleanup
lots
---
.../software/SPDF/SPdfApplication.java | 2 +-
.../SPDF/controller/api/CropController.java | 132 ++++
.../api/other/PageNumbersController.java | 4 +-
.../controller/api/pipeline/Controller.java | 576 ++++++++++--------
.../controller/web/GeneralWebController.java | 6 +
.../SPDF/model/PipelineOperation.java | 7 +
src/main/resources/messages_en_GB.properties | 3 +
src/main/resources/static/images/crop.svg | 3 +
src/main/resources/templates/crop.html | 105 ++++
.../resources/templates/fragments/navbar.html | 7 +-
src/main/resources/templates/home.html | 2 +-
.../templates/other/add-page-numbers.html | 122 +++-
.../templates/security/add-watermark.html | 2 +-
13 files changed, 699 insertions(+), 272 deletions(-)
create mode 100644 src/main/java/stirling/software/SPDF/controller/api/CropController.java
create mode 100644 src/main/resources/static/images/crop.svg
create mode 100644 src/main/resources/templates/crop.html
diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java
index 6a953e49..f4c8c7c5 100644
--- a/src/main/java/stirling/software/SPDF/SPdfApplication.java
+++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java
@@ -15,7 +15,7 @@ import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.utils.GeneralUtils;
@SpringBootApplication
-//@EnableScheduling
+@EnableScheduling
public class SPdfApplication {
@Autowired
diff --git a/src/main/java/stirling/software/SPDF/controller/api/CropController.java b/src/main/java/stirling/software/SPDF/controller/api/CropController.java
new file mode 100644
index 00000000..e56c1b40
--- /dev/null
+++ b/src/main/java/stirling/software/SPDF/controller/api/CropController.java
@@ -0,0 +1,132 @@
+package stirling.software.SPDF.controller.api;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RequestPart;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import stirling.software.SPDF.utils.GeneralUtils;
+import stirling.software.SPDF.utils.WebResponseUtils;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.multipart.MultipartFile;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import com.itextpdf.kernel.geom.PageSize;
+import com.itextpdf.kernel.geom.Rectangle;
+import com.itextpdf.kernel.pdf.PdfDocument;
+import com.itextpdf.kernel.pdf.PdfPage;
+import com.itextpdf.kernel.pdf.PdfReader;
+import com.itextpdf.kernel.pdf.PdfWriter;
+import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
+import com.itextpdf.kernel.pdf.canvas.parser.EventType;
+import com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor;
+import com.itextpdf.kernel.pdf.canvas.parser.data.IEventData;
+import com.itextpdf.kernel.pdf.canvas.parser.data.TextRenderInfo;
+import com.itextpdf.kernel.pdf.canvas.parser.listener.IEventListener;
+import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
+
+import io.swagger.v3.oas.annotations.Hidden;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import stirling.software.SPDF.utils.WebResponseUtils;
+
+@RestController
+@Tag(name = "General", description = "General APIs")
+public class CropController {
+
+ private static final Logger logger = LoggerFactory.getLogger(CropController.class);
+
+
+ @PostMapping(value = "/crop", consumes = "multipart/form-data")
+ @Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")
+ public ResponseEntity cropPdf(
+ @Parameter(description = "The input PDF file", required = true) @RequestParam("file") MultipartFile file,
+ @Parameter(description = "The x-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("x") float x,
+ @Parameter(description = "The y-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("y") float y,
+ @Parameter(description = "The width of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("width") float width,
+ @Parameter(description = "The height of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("height") float height) throws IOException {
+ byte[] bytes = file.getBytes();
+ System.out.println("x=" + x + ", " + "y=" + y + ", " + "width=" + width + ", " +"height=" + height );
+ PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes));
+ PdfDocument pdfDoc = new PdfDocument(reader);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ PdfWriter writer = new PdfWriter(baos);
+ PdfDocument outputPdf = new PdfDocument(writer);
+
+ int totalPages = pdfDoc.getNumberOfPages();
+
+ for (int i = 1; i <= totalPages; i++) {
+ PdfPage page = outputPdf.addNewPage(new PageSize(width, height));
+ PdfCanvas pdfCanvas = new PdfCanvas(page);
+
+ PdfFormXObject formXObject = pdfDoc.getPage(i).copyAsFormXObject(outputPdf);
+
+ // Save the graphics state, apply the transformations, add the object, and then
+ // restore the graphics state
+ pdfCanvas.saveState();
+ pdfCanvas.rectangle(x, y, width, height);
+ pdfCanvas.clip();
+ pdfCanvas.addXObject(formXObject, -x, -y);
+ pdfCanvas.restoreState();
+ }
+
+
+ outputPdf.close();
+ byte[] pdfContent = baos.toByteArray();
+ pdfDoc.close();
+ return WebResponseUtils.bytesToWebResponse(pdfContent,
+ file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf");
+ }
+
+}
diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java b/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java
index ab27d9a0..3a4b5b1c 100644
--- a/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java
+++ b/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java
@@ -114,9 +114,9 @@ public class PageNumbersController {
for (int i : pagesToNumberList) {
PdfPage page = pdfDoc.getPage(i+1);
Rectangle pageSize = page.getPageSize();
- PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamBefore(), page.getResources(), pdfDoc);
+ PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamAfter(), page.getResources(), pdfDoc);
- String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{p}", String.valueOf(pdfDoc.getNumberOfPages())) : String.valueOf(pageNumber);
+ String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(pdfDoc.getNumberOfPages())) : String.valueOf(pageNumber);
PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA);
float textWidth = font.getWidth(text, fontSize);
diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/Controller.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/Controller.java
index d0325450..308927e6 100644
--- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/Controller.java
+++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/Controller.java
@@ -20,7 +20,10 @@ import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
-
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
@@ -48,247 +51,328 @@ import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation;
import stirling.software.SPDF.utils.WebResponseUtils;
-
@RestController
@Tag(name = "Pipeline", description = "Pipeline APIs")
public class Controller {
+ private static final Logger logger = LoggerFactory.getLogger(Controller.class);
@Autowired
private ObjectMapper objectMapper;
-
-
- final String jsonFileName = "pipelineCofig.json";
+
+ final String jsonFileName = "pipelineConfig.json";
final String watchedFoldersDir = "watchedFolders/";
- @Scheduled(fixedRate = 5000)
+ final String finishedFoldersDir = "finishedFolders/";
+
+ @Scheduled(fixedRate = 25000)
public void scanFolders() {
+ logger.info("Scanning folders...");
Path watchedFolderPath = Paths.get(watchedFoldersDir);
- if (!Files.exists(watchedFolderPath)) {
- try {
- Files.createDirectories(watchedFolderPath);
- } catch (IOException e) {
- e.printStackTrace();
- return;
- }
- }
-
+ 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 -> {
+ paths.filter(Files::isDirectory).forEach(t -> {
try {
if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) {
handleDirectory(t);
}
} catch (Exception e) {
- e.printStackTrace();
+ logger.error("Error handling directory: {}", t, e);
}
});
- } catch (Exception e) {
- e.printStackTrace();
- }
+ } catch (Exception e) {
+ logger.error("Error walking through directory: {}", watchedFolderPath, e);
+ }
}
-
+
private void handleDirectory(Path dir) throws Exception {
- Path jsonFile = dir.resolve(jsonFileName);
- Path processingDir = dir.resolve("processing"); // Directory to move files during processing
- if (!Files.exists(processingDir)) {
- Files.createDirectory(processingDir);
- }
-
- if (Files.exists(jsonFile)) {
- // Read JSON file
- String jsonString;
- try {
- jsonString = new String(Files.readAllBytes(jsonFile));
- } catch (IOException e) {
- e.printStackTrace();
- return;
- }
-
- // Decode JSON to PipelineConfig
- PipelineConfig config;
- try {
- config = objectMapper.readValue(jsonString, PipelineConfig.class);
- // Assuming your PipelineConfig class has getters for all necessary fields, you can perform checks here
- if (config.getOperations() == null || config.getOutputDir() == null || config.getName() == null) {
- throw new IOException("Invalid JSON format");
- }
- } catch (IOException e) {
- e.printStackTrace();
- return;
- }
-
- // For each operation in the pipeline
- for (PipelineOperation operation : config.getOperations()) {
- // Collect all files based on fileInput
- File[] files;
- String fileInput = (String) operation.getParameters().get("fileInput");
- if ("automated".equals(fileInput)) {
- // If fileInput is "automated", process all files in the directory
- try (Stream paths = Files.list(dir)) {
- files = paths.filter(path -> !path.equals(jsonFile))
- .map(Path::toFile)
- .toArray(File[]::new);
- } catch (IOException e) {
- e.printStackTrace();
- return;
- }
- } else {
- // If fileInput contains a path, process only this file
- files = new File[]{new File(fileInput)};
- }
-
- // Prepare the files for processing
- File[] filesToProcess = files.clone();
- for (File file : filesToProcess) {
- Files.move(file.toPath(), processingDir.resolve(file.getName()));
- }
-
- // Process the files
- try {
- List resources = handleFiles(filesToProcess, jsonString);
-
- // Move resultant files and rename them as per config in JSON file
- for (Resource resource : resources) {
- String outputFileName = config.getOutputPattern().replace("{filename}", resource.getFile().getName());
- outputFileName = outputFileName.replace("{pipelineName}", config.getName());
- DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
- outputFileName = outputFileName.replace("{date}", LocalDate.now().format(dateFormatter));
- DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HHmmss");
- outputFileName = outputFileName.replace("{time}", LocalTime.now().format(timeFormatter));
- // {filename} {folder} {date} {tmime} {pipeline}
-
- Files.move(resource.getFile().toPath(), Paths.get(config.getOutputDir(), outputFileName));
- }
-
- // If successful, delete the original files
- for (File file : filesToProcess) {
- Files.deleteIfExists(processingDir.resolve(file.getName()));
- }
- } catch (Exception e) {
- // If an error occurs, move the original files back
- for (File file : filesToProcess) {
- Files.move(processingDir.resolve(file.getName()), file.toPath());
- }
- throw e;
- }
- }
- }
- }
-
-
-
-
-List processFiles(List outputFiles, String jsonString) throws Exception{
- ObjectMapper mapper = new ObjectMapper();
- JsonNode jsonNode = mapper.readTree(jsonString);
-
- JsonNode pipelineNode = jsonNode.get("pipeline");
- ByteArrayOutputStream logStream = new ByteArrayOutputStream();
- PrintStream logPrintStream = new PrintStream(logStream);
-
- boolean hasErrors = false;
-
- for (JsonNode operationNode : pipelineNode) {
- String operation = operationNode.get("operation").asText();
- JsonNode parametersNode = operationNode.get("parameters");
- String inputFileExtension = "";
- if(operationNode.has("inputFileType")) {
- inputFileExtension = operationNode.get("inputFileType").asText();
- } else {
- inputFileExtension=".pdf";
+ logger.info("Handling directory: {}", dir);
+ Path jsonFile = dir.resolve(jsonFileName);
+ Path processingDir = dir.resolve("processing"); // Directory to move files during processing
+ if (!Files.exists(processingDir)) {
+ Files.createDirectory(processingDir);
+ logger.info("Created processing directory: {}", processingDir);
}
- List newOutputFiles = new ArrayList<>();
- boolean hasInputFileType = false;
-
- for (Resource file : outputFiles) {
- if (file.getFilename().endsWith(inputFileExtension)) {
- hasInputFileType = true;
- MultiValueMap body = new LinkedMultiValueMap<>();
- body.add("fileInput", file);
-
- Iterator> parameters = parametersNode.fields();
- while (parameters.hasNext()) {
- Map.Entry parameter = parameters.next();
- body.add(parameter.getKey(), parameter.getValue().asText());
- }
-
- HttpHeaders headers = new HttpHeaders();
- headers.setContentType(MediaType.MULTIPART_FORM_DATA);
-
- HttpEntity> entity = new HttpEntity<>(body, headers);
-
- RestTemplate restTemplate = new RestTemplate();
- String url = "http://localhost:8080/" + operation;
-
- ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
-
- if (!response.getStatusCode().equals(HttpStatus.OK)) {
- logPrintStream.println("Error: " + response.getBody());
- hasErrors = true;
- continue;
- }
-
- // Check if the response body is a zip file
- if (isZip(response.getBody())) {
- // Unzip the file and add all the files to the new output files
- newOutputFiles.addAll(unzip(response.getBody()));
- } else {
- Resource outputResource = new ByteArrayResource(response.getBody()) {
- @Override
- public String getFilename() {
- return file.getFilename(); // Preserving original filename
- }
- };
- newOutputFiles.add(outputResource);
- }
+ if (Files.exists(jsonFile)) {
+ // Read JSON file
+ String jsonString;
+ try {
+ jsonString = new String(Files.readAllBytes(jsonFile));
+ logger.info("Read JSON file: {}", jsonFile);
+ } catch (IOException e) {
+ logger.error("Error reading JSON file: {}", jsonFile, e);
+ return;
}
- if (!hasInputFileType) {
- logPrintStream.println("No files with extension " + inputFileExtension + " found for operation " + operation);
- hasErrors = true;
- }
-
- outputFiles = newOutputFiles;
+ // Decode JSON to PipelineConfig
+ PipelineConfig config;
+ try {
+ config = objectMapper.readValue(jsonString, PipelineConfig.class);
+ // Assuming your PipelineConfig class has getters for all necessary fields, you
+ // can perform checks here
+ if (config.getOperations() == null || config.getOutputDir() == null || config.getName() == null) {
+ throw new IOException("Invalid JSON format");
+ }
+ logger.info("Parsed PipelineConfig: {}", config);
+ } catch (IOException e) {
+ logger.error("Error parsing PipelineConfig: {}", jsonString, e);
+ return;
+ }
+
+ // For each operation in the pipeline
+ for (PipelineOperation operation : config.getOperations()) {
+ logger.info("Processing operation: {}", operation.toString());
+ // Collect all files based on fileInput
+ File[] files;
+ String fileInput = (String) operation.getParameters().get("fileInput");
+ if ("automated".equals(fileInput)) {
+ // If fileInput is "automated", process all files in the directory
+ try (Stream paths = Files.list(dir)) {
+ files = paths
+ .filter(path -> !Files.isDirectory(path)) // exclude directories
+ .filter(path -> !path.equals(jsonFile)) // exclude jsonFile
+ .map(Path::toFile)
+ .toArray(File[]::new);
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ return;
+ }
+ } else {
+ // If fileInput contains a path, process only this file
+ files = new File[] { new File(fileInput) };
+ }
+
+ // Prepare the files for processing
+ List filesToProcess = new ArrayList<>();
+ for (File file : files) {
+ logger.info(file.getName());
+ logger.info("{} to {}",file.toPath(), processingDir.resolve(file.getName()));
+ Files.move(file.toPath(), processingDir.resolve(file.getName()));
+ filesToProcess.add(processingDir.resolve(file.getName()).toFile());
+ }
+
+ // Process the files
+ try {
+ List resources = handleFiles(filesToProcess.toArray(new File[0]), jsonString);
+
+ if(resources == null) {
+ return;
+ }
+ // Move resultant files and rename them as per config in JSON file
+ for (Resource resource : resources) {
+ String resourceName = resource.getFilename();
+ String baseName = resourceName.substring(0, resourceName.lastIndexOf("."));
+ String extension = resourceName.substring(resourceName.lastIndexOf(".")+1);
+
+ String outputFileName = config.getOutputPattern().replace("{filename}", baseName);
+
+ outputFileName = outputFileName.replace("{pipelineName}", config.getName());
+ DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
+ outputFileName = outputFileName.replace("{date}", LocalDate.now().format(dateFormatter));
+ DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HHmmss");
+ outputFileName = outputFileName.replace("{time}", LocalTime.now().format(timeFormatter));
+
+ outputFileName += "." + extension;
+ // {filename} {folder} {date} {tmime} {pipeline}
+ String outputDir = config.getOutputDir();
+
+ // Check if the environment variable 'automatedOutputFolder' is set
+ String outputFolder = System.getenv("automatedOutputFolder");
+
+ if (outputFolder == null || outputFolder.isEmpty()) {
+ // If the environment variable is not set, use the default value
+ outputFolder = finishedFoldersDir;
+ }
+
+ // Replace the placeholders in the outputDir string
+ outputDir = outputDir.replace("{outputFolder}", outputFolder);
+ outputDir = outputDir.replace("{folderName}", dir.toString());
+ outputDir = outputDir.replace("\\watchedFolders", "");
+ Path outputPath;
+
+ if (Paths.get(outputDir).isAbsolute()) {
+ // If it's an absolute path, use it directly
+ outputPath = Paths.get(outputDir);
+ } else {
+ // If it's a relative path, make it relative to the current working directory
+ outputPath = Paths.get(".", outputDir);
+ }
+
+
+ if (!Files.exists(outputPath)) {
+ try {
+ Files.createDirectories(outputPath);
+ logger.info("Created directory: {}", outputPath);
+ } catch (IOException e) {
+ logger.error("Error creating directory: {}", outputPath, e);
+ return;
+ }
+ }
+ logger.info("outputPath {}", outputPath);
+ logger.info("outputPath.resolve(outputFileName).toString() {}", outputPath.resolve(outputFileName).toString());
+ File newFile = new File(outputPath.resolve(outputFileName).toString());
+ OutputStream os = new FileOutputStream(newFile);
+ os.write(((ByteArrayResource)resource).getByteArray());
+ os.close();
+ logger.info("made {}", outputPath.resolve(outputFileName));
+ }
+
+ // If successful, delete the original files
+ for (File file : filesToProcess) {
+ Files.deleteIfExists(processingDir.resolve(file.getName()));
+ }
+ } catch (Exception e) {
+ // If an error occurs, move the original files back
+ for (File file : filesToProcess) {
+ Files.move(processingDir.resolve(file.getName()), file.toPath());
+ }
+ throw e;
+ }
+ }
}
- logPrintStream.close();
+ }
+
+ List processFiles(List outputFiles, String jsonString) throws Exception {
- }
- return outputFiles;
-}
-
-
-List handleFiles(File[] files, String jsonString) throws Exception{
- ObjectMapper mapper = new ObjectMapper();
- JsonNode jsonNode = mapper.readTree(jsonString);
-
- JsonNode pipelineNode = jsonNode.get("pipeline");
- ByteArrayOutputStream logStream = new ByteArrayOutputStream();
- PrintStream logPrintStream = new PrintStream(logStream);
-
- boolean hasErrors = false;
- List outputFiles = new ArrayList<>();
-
- for (File file : files) {
- Path path = Paths.get(file.getAbsolutePath());
- Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) {
- @Override
- public String getFilename() {
- return file.getName();
- }
- };
- outputFiles.add(fileResource);
- }
- return processFiles(outputFiles, jsonString);
-}
-
- List handleFiles(MultipartFile[] files, String jsonString) throws Exception{
+ logger.info("Processing files... " + outputFiles);
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(jsonString);
JsonNode pipelineNode = jsonNode.get("pipeline");
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
PrintStream logPrintStream = new PrintStream(logStream);
+
+ boolean hasErrors = false;
+
+ for (JsonNode operationNode : pipelineNode) {
+ String operation = operationNode.get("operation").asText();
+ logger.info("Running operation: {}", operation);
+ JsonNode parametersNode = operationNode.get("parameters");
+ String inputFileExtension = "";
+ if (operationNode.has("inputFileType")) {
+ inputFileExtension = operationNode.get("inputFileType").asText();
+ } else {
+ inputFileExtension = ".pdf";
+ }
+
+ List newOutputFiles = new ArrayList<>();
+ boolean hasInputFileType = false;
+
+ for (Resource file : outputFiles) {
+ if (file.getFilename().endsWith(inputFileExtension)) {
+ hasInputFileType = true;
+ MultiValueMap body = new LinkedMultiValueMap<>();
+ body.add("fileInput", file);
+
+ Iterator> parameters = parametersNode.fields();
+ while (parameters.hasNext()) {
+ Map.Entry parameter = parameters.next();
+ body.add(parameter.getKey(), parameter.getValue().asText());
+ }
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.MULTIPART_FORM_DATA);
+
+ HttpEntity> entity = new HttpEntity<>(body, headers);
+
+ RestTemplate restTemplate = new RestTemplate();
+ String url = "http://localhost:8080/" + operation;
+
+ ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
+
+ if (!response.getStatusCode().equals(HttpStatus.OK)) {
+ logPrintStream.println("Error: " + response.getBody());
+ hasErrors = true;
+ continue;
+ }
+
+ // Check if the response body is a zip file
+ if (isZip(response.getBody())) {
+ // Unzip the file and add all the files to the new output files
+ newOutputFiles.addAll(unzip(response.getBody()));
+ } else {
+ Resource outputResource = new ByteArrayResource(response.getBody()) {
+ @Override
+ public String getFilename() {
+ return file.getFilename(); // Preserving original filename
+ }
+ };
+ newOutputFiles.add(outputResource);
+ }
+ }
+
+ if (!hasInputFileType) {
+ logPrintStream.println(
+ "No files with extension " + inputFileExtension + " found for operation " + operation);
+ hasErrors = true;
+ }
+
+ outputFiles = newOutputFiles;
+ }
+ logPrintStream.close();
+
+ }
+ if (hasErrors) {
+ logger.error("Errors occurred during processing. Log: {}", logStream.toString());
+ }
+ return outputFiles;
+ }
+
+ List handleFiles(File[] files, String jsonString) throws Exception {
+ if(files == null || files.length == 0) {
+ logger.info("No files");
+ return null;
+ }
+
+ logger.info("Handling files: {} files, with JSON string of length: {}", files.length, jsonString.length());
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode jsonNode = mapper.readTree(jsonString);
+
+ JsonNode pipelineNode = jsonNode.get("pipeline");
+
+ boolean hasErrors = false;
+ List outputFiles = new ArrayList<>();
+
+ for (File file : files) {
+ Path path = Paths.get(file.getAbsolutePath());
+ System.out.println("Reading file: " + path); // debug statement
+
+ if (Files.exists(path)) {
+ Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) {
+ @Override
+ public String getFilename() {
+ return file.getName();
+ }
+ };
+ outputFiles.add(fileResource);
+ } else {
+ System.out.println("File not found: " + path); // debug statement
+ }
+ }
+ logger.info("Files successfully loaded. Starting processing...");
+ return processFiles(outputFiles, jsonString);
+ }
+
+ List handleFiles(MultipartFile[] files, String jsonString) throws Exception {
+ if(files == null || files.length == 0) {
+ logger.info("No files");
+ return null;
+ }
+ logger.info("Handling files: {} files, with JSON string of length: {}", files.length, jsonString.length());
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode jsonNode = mapper.readTree(jsonString);
+
+ JsonNode pipelineNode = jsonNode.get("pipeline");
+
boolean hasErrors = false;
List outputFiles = new ArrayList<>();
@@ -301,53 +385,59 @@ List handleFiles(File[] files, String jsonString) throws Exception{
};
outputFiles.add(fileResource);
}
+ logger.info("Files successfully loaded. Starting processing...");
return processFiles(outputFiles, jsonString);
}
-
+
@PostMapping("/handleData")
public ResponseEntity handleData(@RequestPart("fileInput") MultipartFile[] files,
@RequestParam("json") String jsonString) {
+ logger.info("Received POST request to /handleData with {} files", files.length);
try {
-
- List outputFiles = handleFiles(files, jsonString);
+ List outputFiles = handleFiles(files, jsonString);
- if (outputFiles.size() == 1) {
- // If there is only one file, return it directly
- Resource singleFile = outputFiles.get(0);
- InputStream is = singleFile.getInputStream();
- byte[] bytes = new byte[(int)singleFile.contentLength()];
- is.read(bytes);
- is.close();
-
- return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM);
- }
+ if (outputFiles != null && outputFiles.size() == 1) {
+ // If there is only one file, return it directly
+ Resource singleFile = outputFiles.get(0);
+ InputStream is = singleFile.getInputStream();
+ byte[] bytes = new byte[(int) singleFile.contentLength()];
+ is.read(bytes);
+ is.close();
+
+ logger.info("Returning single file response...");
+ return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(),
+ MediaType.APPLICATION_OCTET_STREAM);
+ } else if (outputFiles == null) {
+ return null;
+ }
// Create a ByteArrayOutputStream to hold the zip
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- ZipOutputStream zipOut = new ZipOutputStream(baos);
+ 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();
-
+ zipOut.close();
+
+ logger.info("Returning zipped file response...");
return WebResponseUtils.boasToWebResponse(baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
} catch (Exception e) {
- e.printStackTrace();
+ logger.error("Error handling data: ", e);
return null;
}
}
@@ -362,6 +452,7 @@ List handleFiles(File[] files, String jsonString) throws Exception{
}
private List unzip(byte[] data) throws IOException {
+ logger.info("Unzipping data of length: {}", data.length);
List unzippedFiles = new ArrayList<>();
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
@@ -387,6 +478,7 @@ List handleFiles(File[] files, String jsonString) throws Exception{
// 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);
@@ -394,6 +486,8 @@ List handleFiles(File[] files, String jsonString) throws Exception{
}
}
+ logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
return unzippedFiles;
}
+
}
diff --git a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java
index 11a7c9bf..f30c54e5 100644
--- a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java
+++ b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java
@@ -68,4 +68,10 @@ public class GeneralWebController {
return "sign";
}
+ @GetMapping("/crop")
+ @Hidden
+ public String cropForm(Model model) {
+ model.addAttribute("currentPage", "crop");
+ return "crop";
+ }
}
diff --git a/src/main/java/stirling/software/SPDF/model/PipelineOperation.java b/src/main/java/stirling/software/SPDF/model/PipelineOperation.java
index 8b079ba1..10c27bfc 100644
--- a/src/main/java/stirling/software/SPDF/model/PipelineOperation.java
+++ b/src/main/java/stirling/software/SPDF/model/PipelineOperation.java
@@ -22,4 +22,11 @@ public class PipelineOperation {
public void setParameters(Map parameters) {
this.parameters = parameters;
}
+
+ @Override
+ public String toString() {
+ return "PipelineOperation [operation=" + operation + ", parameters=" + parameters + "]";
+ }
+
+
}
\ No newline at end of file
diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties
index ce7ee576..2a6941f6 100644
--- a/src/main/resources/messages_en_GB.properties
+++ b/src/main/resources/messages_en_GB.properties
@@ -146,6 +146,9 @@ home.auto-rename.desc=Auto renames a PDF file based on its detected header
home.adjust-contrast.title=Adjust Colors/Contrast
home.adjust-contrast.desc=Adjust Contrast, Saturation and Brightness of a PDF
+home.crop.title=Crop PDF
+home.crop.desc=Crop a PDF to reduce its size (maintains text!)
+
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect
downloadPdf=Download PDF
diff --git a/src/main/resources/static/images/crop.svg b/src/main/resources/static/images/crop.svg
new file mode 100644
index 00000000..b7e17490
--- /dev/null
+++ b/src/main/resources/static/images/crop.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/src/main/resources/templates/crop.html b/src/main/resources/templates/crop.html
new file mode 100644
index 00000000..b3abeba3
--- /dev/null
+++ b/src/main/resources/templates/crop.html
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
\ 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 226141f6..7b3902cc 100644
--- a/src/main/resources/templates/fragments/navbar.html
+++ b/src/main/resources/templates/fragments/navbar.html
@@ -40,7 +40,7 @@
-->
-
+
@@ -98,13 +98,14 @@
-
+
diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html
index 145b65d1..012070c6 100644
--- a/src/main/resources/templates/home.html
+++ b/src/main/resources/templates/home.html
@@ -76,7 +76,7 @@
-
+
diff --git a/src/main/resources/templates/other/add-page-numbers.html b/src/main/resources/templates/other/add-page-numbers.html
index 59cd0403..2ea91a3e 100644
--- a/src/main/resources/templates/other/add-page-numbers.html
+++ b/src/main/resources/templates/other/add-page-numbers.html
@@ -15,30 +15,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/templates/security/add-watermark.html b/src/main/resources/templates/security/add-watermark.html
index 30091ea8..530388c9 100644
--- a/src/main/resources/templates/security/add-watermark.html
+++ b/src/main/resources/templates/security/add-watermark.html
@@ -22,7 +22,7 @@