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 @@
- icon + icon @@ -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 @@ -
+
+
diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index 33bf6b42..145b65d1 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -20,8 +20,13 @@

-
- + + +
+ +
+ +
@@ -70,13 +75,13 @@
- +
- +
-
+
diff --git a/src/main/resources/templates/other/adjust-contrast.html b/src/main/resources/templates/other/adjust-contrast.html index 87496d4f..8250d609 100644 --- a/src/main/resources/templates/other/adjust-contrast.html +++ b/src/main/resources/templates/other/adjust-contrast.html @@ -13,15 +13,215 @@

+ + -
-
-
- - -
- -
+

Contrast: 100%

+ + +

Brightness: 100%

+ + +

Saturation: 100%

+ + + + + + + +
From 5877b40be5f59936d20f288df096717052205d18 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Thu, 6 Jul 2023 22:52:22 +0100 Subject: [PATCH 04/26] adjust contrast! --- src/main/resources/messages_en_GB.properties | 2 + .../templates/other/adjust-contrast.html | 204 +++++++++++------- 2 files changed, 134 insertions(+), 72 deletions(-) diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 15ff69d3..ce7ee576 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -143,6 +143,8 @@ home.add-page-numbers.desc=Add Page numbers throughout a document in a set locat home.auto-rename.title=Auto Rename PDF File 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 error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect diff --git a/src/main/resources/templates/other/adjust-contrast.html b/src/main/resources/templates/other/adjust-contrast.html index 8250d609..763ecf65 100644 --- a/src/main/resources/templates/other/adjust-contrast.html +++ b/src/main/resources/templates/other/adjust-contrast.html @@ -1,64 +1,95 @@ - + - + -
-
-
-

-
-
-
-

- - +
+
+
+

+
+
+
+

+ + -

Contrast: 100%

- +

+ Contrast: 100% +

+ -

Brightness: 100%

- +

+ Brightness: 100% +

+ -

Saturation: 100%

- - - +

+ Saturation: 100% +

+ - - + - - -
-
-
-
-
-
+ + +
+
+
+
+
+
\ 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 @@ --> - 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 @@

-
-
- - -
- - -
- - -
- - -
- - -
- -
+
+
+
+ + +
+ + + +
+ +
+
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
+ +
+
+ +
+ + + +
+ + +
+
+ + +
+
+ + +
+ +
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 @@
+
+ + +
@@ -196,20 +200,14 @@ DraggableUtils.createDraggableCanvasFromUrl(dataURL); } - + + + +
From 94526de04b448eb78a7a4ca67c334a36b37c02ce Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sun, 9 Jul 2023 20:34:07 +0100 Subject: [PATCH 08/26] sign fix --- src/main/resources/templates/sign.html | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/main/resources/templates/sign.html b/src/main/resources/templates/sign.html index bd072850..b879430e 100644 --- a/src/main/resources/templates/sign.html +++ b/src/main/resources/templates/sign.html @@ -19,6 +19,13 @@ } + @@ -200,6 +207,29 @@ DraggableUtils.createDraggableCanvasFromUrl(dataURL); } + + + -
- - - + + + + + +
+
+
+

+
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+ + \ No newline at end of file From 5d926b022b683b28cda72768a8292d163850b715 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 12 Jul 2023 00:17:55 +0100 Subject: [PATCH 10/26] gitingore --- .gitignore | 242 +++++++++++++++++++++++++++-------------------------- 1 file changed, 122 insertions(+), 120 deletions(-) diff --git a/.gitignore b/.gitignore index ff36fc93..e874e870 100644 --- a/.gitignore +++ b/.gitignore @@ -1,121 +1,123 @@ - - -### Eclipse ### -.metadata -bin/ -tmp/ -*.tmp -*.bak -*.swp -*~.nib -local.properties -.settings/ -.loadpath -.recommenders -.classpath -.project -version.properties - -#### Stirling-PDF Files ### -customFiles/ -config/ -watchedFolders/ - - -# Gradle -.gradle -.lock - -# External tool builders -.externalToolBuilders/ - -# Locally stored "Eclipse launch configurations" -*.launch - -# PyDev specific (Python IDE for Eclipse) -*.pydevproject - -# CDT-specific (C/C++ Development Tooling) -.cproject - -# CDT- autotools -.autotools - -# Java annotation processor (APT) -.factorypath - -# PDT-specific (PHP Development Tools) -.buildpath - -# sbteclipse plugin -.target - -# Tern plugin -.tern-project - -# TeXlipse plugin -.texlipse - -# STS (Spring Tool Suite) -.springBeans - -# Code Recommenders -.recommenders/ - -# Annotation Processing -.apt_generated/ -.apt_generated_test/ - -# Scala IDE specific (Scala & Java development for Eclipse) -.cache-main -.scala_dependencies -.worksheet - -# Uncomment this line if you wish to ignore the project description file. -# Typically, this file would be tracked if it contains build/dependency configurations: -#.project - -### Eclipse Patch ### -# Spring Boot Tooling -.sts4-cache/ - -### Git ### -# Created by git for backups. To disable backups in Git: -# $ git config --global mergetool.keepBackup false -*.orig - -# Created by git when using merge tools for conflicts -*.BACKUP.* -*.BASE.* -*.LOCAL.* -*.REMOTE.* -*_BACKUP_*.txt -*_BASE_*.txt -*_LOCAL_*.txt -*_REMOTE_*.txt - -### Java ### -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -/build - + + +### Eclipse ### +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders +.classpath +.project +version.properties +*.pdf +pipeline/ + +#### Stirling-PDF Files ### +customFiles/ +config/ +watchedFolders/ + + +# Gradle +.gradle +.lock + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +# Uncomment this line if you wish to ignore the project description file. +# Typically, this file would be tracked if it contains build/dependency configurations: +#.project + +### Eclipse Patch ### +# Spring Boot Tooling +.sts4-cache/ + +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +/build + /.vscode \ No newline at end of file From cdbf1fa73a8fe73730d535197bfa7c2fdda16090 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 12 Jul 2023 23:27:36 +0100 Subject: [PATCH 11/26] watermark features --- .../api/security/WatermarkController.java | 224 +++++++++--------- .../templates/security/add-watermark.html | 62 +++-- 2 files changed, 162 insertions(+), 124 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java b/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java index 4ef8604b..962f578e 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java @@ -1,12 +1,15 @@ package stirling.software.SPDF.controller.api.security; import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.util.Arrays; -import java.util.List; + +import javax.imageio.ImageIO; import org.apache.commons.io.IOUtils; import org.apache.pdfbox.pdmodel.PDDocument; @@ -15,6 +18,8 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.font.PDType0Font; import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; import org.apache.pdfbox.util.Matrix; import org.springframework.core.io.ClassPathResource; @@ -30,124 +35,127 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import stirling.software.SPDF.utils.WebResponseUtils; import io.swagger.v3.oas.annotations.media.Schema; + @RestController @Tag(name = "Security", description = "Security APIs") public class WatermarkController { - @PostMapping(consumes = "multipart/form-data", value = "/add-watermark") - @Operation(summary = "Add watermark to a PDF file", - description = "This endpoint adds a watermark to a given PDF file. Users can specify the watermark text, font size, rotation, opacity, width spacer, and height spacer. Input:PDF Output:PDF Type:SISO") - public ResponseEntity addWatermark( - @RequestPart(required = true, value = "fileInput") - @Parameter(description = "The input PDF file to add a watermark") - MultipartFile pdfFile, - @RequestParam(defaultValue = "roman", name = "alphabet") - @Parameter(description = "The selected alphabet", - schema = @Schema(type = "string", - allowableValues = {"roman","arabic","japanese","korean","chinese"}, - defaultValue = "roman")) - String alphabet, - @RequestParam("watermarkText") - @Parameter(description = "The watermark text to add to the PDF file") - String watermarkText, - @RequestParam(defaultValue = "30", name = "fontSize") - @Parameter(description = "The font size of the watermark text", example = "30") - float fontSize, - @RequestParam(defaultValue = "0", name = "rotation") - @Parameter(description = "The rotation of the watermark text in degrees", example = "0") - float rotation, - @RequestParam(defaultValue = "0.5", name = "opacity") - @Parameter(description = "The opacity of the watermark text (0.0 - 1.0)", example = "0.5") - float opacity, - @RequestParam(defaultValue = "50", name = "widthSpacer") - @Parameter(description = "The width spacer between watermark texts", example = "50") - int widthSpacer, - @RequestParam(defaultValue = "50", name = "heightSpacer") - @Parameter(description = "The height spacer between watermark texts", example = "50") - int heightSpacer) throws IOException, Exception { + @PostMapping(consumes = "multipart/form-data", value = "/add-watermark") + @Operation(summary = "Add watermark to a PDF file", description = "This endpoint adds a watermark to a given PDF file. Users can specify the watermark type (text or image), rotation, opacity, width spacer, and height spacer. Input:PDF Output:PDF Type:SISO") + public ResponseEntity addWatermark( + @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to add a watermark") MultipartFile pdfFile, + @RequestPart(required = true) @Parameter(description = "The watermark type (text or image)") String watermarkType, + @RequestPart(required = false) @Parameter(description = "The watermark text") String watermarkText, + @RequestPart(required = false) @Parameter(description = "The watermark image") MultipartFile watermarkImage, + @RequestParam(defaultValue = "30", name = "fontSize") @Parameter(description = "The font size of the watermark text", example = "30") float fontSize, + @RequestParam(defaultValue = "0", name = "rotation") @Parameter(description = "The rotation of the watermark in degrees", example = "0") float rotation, + @RequestParam(defaultValue = "0.5", name = "opacity") @Parameter(description = "The opacity of the watermark (0.0 - 1.0)", example = "0.5") float opacity, + @RequestParam(defaultValue = "50", name = "widthSpacer") @Parameter(description = "The width spacer between watermark elements", example = "50") int widthSpacer, + @RequestParam(defaultValue = "50", name = "heightSpacer") @Parameter(description = "The height spacer between watermark elements", example = "50") int heightSpacer) + throws IOException, Exception { - // Load the input PDF - PDDocument document = PDDocument.load(pdfFile.getInputStream()); - String producer = document.getDocumentInformation().getProducer(); - // Create a page in the document - for (PDPage page : document.getPages()) { + // Load the input PDF + PDDocument document = PDDocument.load(pdfFile.getInputStream()); - // Get the page's content stream - PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true); + // Create a page in the document + for (PDPage page : document.getPages()) { - // Set transparency - PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState(); - graphicsState.setNonStrokingAlphaConstant(opacity); - contentStream.setGraphicsStateParameters(graphicsState); + // Get the page's content stream + PDPageContentStream contentStream = new PDPageContentStream(document, page, + PDPageContentStream.AppendMode.APPEND, true); + // Set transparency + PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState(); + graphicsState.setNonStrokingAlphaConstant(opacity); + contentStream.setGraphicsStateParameters(graphicsState); - String resourceDir = ""; - PDFont font = PDType1Font.HELVETICA_BOLD; - switch (alphabet) { - case "arabic": - resourceDir = "static/fonts/NotoSansArabic-Regular.ttf"; - break; - case "japanese": - resourceDir = "static/fonts/Meiryo.ttf"; - break; - case "korean": - resourceDir = "static/fonts/malgun.ttf"; - break; - case "chinese": - resourceDir = "static/fonts/SimSun.ttf"; - break; - case "roman": - default: - resourceDir = "static/fonts/NotoSans-Regular.ttf"; - break; - } + if (watermarkType.equalsIgnoreCase("text")) { + addTextWatermark(contentStream, watermarkText, document, page, rotation, widthSpacer, heightSpacer, + fontSize); + } else if (watermarkType.equalsIgnoreCase("image")) { + addImageWatermark(contentStream, watermarkImage, document, page, rotation, widthSpacer, heightSpacer, + fontSize); + } - - if(!resourceDir.equals("")) { - ClassPathResource classPathResource = new ClassPathResource(resourceDir); - String fileExtension = resourceDir.substring(resourceDir.lastIndexOf(".")); - File tempFile = File.createTempFile("NotoSansFont", fileExtension); - try (InputStream is = classPathResource.getInputStream(); FileOutputStream os = new FileOutputStream(tempFile)) { - IOUtils.copy(is, os); - } - - font = PDType0Font.load(document, tempFile); - tempFile.deleteOnExit(); - } - contentStream.beginText(); - contentStream.setFont(font, fontSize); - contentStream.setNonStrokingColor(Color.LIGHT_GRAY); + // Close the content stream + contentStream.close(); + } - // Set size and location of watermark - float pageWidth = page.getMediaBox().getWidth(); - float pageHeight = page.getMediaBox().getHeight(); - float watermarkWidth = widthSpacer + font.getStringWidth(watermarkText) * fontSize / 1000; - float watermarkHeight = heightSpacer + fontSize; - int watermarkRows = (int) (pageHeight / watermarkHeight + 1); - int watermarkCols = (int) (pageWidth / watermarkWidth + 1); + return WebResponseUtils.pdfDocToWebResponse(document, + pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf"); + } - // Add the watermark text - for (int i = 0; i < watermarkRows; i++) { - for (int j = 0; j < watermarkCols; j++) { - - if(producer.contains("Google Docs")) { - //This fixes weird unknown google docs y axis rotation/flip issue - //TODO: Long term fix one day - //contentStream.setTextMatrix(1, 0, 0, -1, j * watermarkWidth, pageHeight - i * watermarkHeight); - Matrix matrix = new Matrix(1, 0, 0, -1, j * watermarkWidth, pageHeight - i * watermarkHeight); - contentStream.setTextMatrix(matrix); - } else { - contentStream.setTextMatrix(Matrix.getRotateInstance((float) Math.toRadians(rotation), j * watermarkWidth, i * watermarkHeight)); - } - contentStream.showTextWithPositioning(new Object[] { watermarkText }); - } - } - contentStream.endText(); + private void addTextWatermark(PDPageContentStream contentStream, String watermarkText, PDDocument document, + PDPage page, float rotation, int widthSpacer, int heightSpacer, float fontSize) throws IOException { + // Set font and other properties for text watermark + PDFont font = PDType1Font.HELVETICA_BOLD; + contentStream.setFont(font, fontSize); + contentStream.setNonStrokingColor(Color.LIGHT_GRAY); - // Close the content stream - contentStream.close(); - } - return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf"); - } + // Set size and location of text watermark + float watermarkWidth = widthSpacer + font.getStringWidth(watermarkText) * fontSize / 1000; + float watermarkHeight = heightSpacer + fontSize; + float pageWidth = page.getMediaBox().getWidth(); + float pageHeight = page.getMediaBox().getHeight(); + int watermarkRows = (int) (pageHeight / watermarkHeight + 1); + int watermarkCols = (int) (pageWidth / watermarkWidth + 1); + + // Add the text watermark + for (int i = 0; i < watermarkRows; i++) { + for (int j = 0; j < watermarkCols; j++) { + contentStream.beginText(); + contentStream.setTextMatrix(Matrix.getRotateInstance((float) Math.toRadians(rotation), + j * watermarkWidth, i * watermarkHeight)); + contentStream.showText(watermarkText); + contentStream.endText(); + } + } + } + + private void addImageWatermark(PDPageContentStream contentStream, MultipartFile watermarkImage, PDDocument document, PDPage page, float rotation, + int widthSpacer, int heightSpacer, float fontSize) throws IOException { + +// Load the watermark image +BufferedImage image = ImageIO.read(watermarkImage.getInputStream()); + +// Compute width based on original aspect ratio +float aspectRatio = (float) image.getWidth() / (float) image.getHeight(); + +// Desired physical height (in PDF points) +float desiredPhysicalHeight = fontSize ; + +// Desired physical width based on the aspect ratio +float desiredPhysicalWidth = desiredPhysicalHeight * aspectRatio; + +// Convert the BufferedImage to PDImageXObject +PDImageXObject xobject = LosslessFactory.createFromImage(document, image); + +// Calculate the number of rows and columns for watermarks +float pageWidth = page.getMediaBox().getWidth(); +float pageHeight = page.getMediaBox().getHeight(); +int watermarkRows = (int) ((pageHeight + heightSpacer) / (desiredPhysicalHeight + heightSpacer)); +int watermarkCols = (int) ((pageWidth + widthSpacer) / (desiredPhysicalWidth + widthSpacer)); + +for (int i = 0; i < watermarkRows; i++) { +for (int j = 0; j < watermarkCols; j++) { +float x = j * (desiredPhysicalWidth + widthSpacer); +float y = i * (desiredPhysicalHeight + heightSpacer); + +// Save the graphics state +contentStream.saveGraphicsState(); + +// Create rotation matrix and rotate +contentStream.transform(Matrix.getTranslateInstance(x + desiredPhysicalWidth / 2, y + desiredPhysicalHeight / 2)); +contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0)); +contentStream.transform(Matrix.getTranslateInstance(-desiredPhysicalWidth / 2, -desiredPhysicalHeight / 2)); + +// Draw the image and restore the graphics state +contentStream.drawImage(xobject, 0, 0, desiredPhysicalWidth, desiredPhysicalHeight); +contentStream.restoreGraphicsState(); +} + +} + + } } diff --git a/src/main/resources/templates/security/add-watermark.html b/src/main/resources/templates/security/add-watermark.html index 530388c9..060805fa 100644 --- a/src/main/resources/templates/security/add-watermark.html +++ b/src/main/resources/templates/security/add-watermark.html @@ -3,7 +3,7 @@ - +
@@ -16,30 +16,36 @@
-
+
+ +
- +
- - + +
-
+ +
+ + +
- +
@@ -48,7 +54,7 @@ const opacityInput = document.getElementById('opacity'); const opacityRealInput = document.getElementById('opacityReal'); - const updateopacityValue = () => { + const updateOpacityValue = () => { let percentageValue = parseFloat(opacityInput.value.replace('%', '')); if (isNaN(percentageValue)) { percentageValue = 0; @@ -68,14 +74,15 @@ opacityInput.value = opacityInput.value.replace('%', ''); }); opacityInput.addEventListener('blur', () => { - updateopacityValue(); + updateOpacityValue(); appendPercentageSymbol(); }); // Set initial values - updateopacityValue(); + updateOpacityValue(); appendPercentageSymbol(); +
@@ -92,6 +99,29 @@
+ + +
From ddf5915c6a986973b788cdcd4246f398a8eb6211 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Thu, 13 Jul 2023 22:03:23 +0100 Subject: [PATCH 12/26] drag drop niceness --- src/main/resources/static/js/fileInput.js | 89 ++++++++++++++++++----- 1 file changed, 71 insertions(+), 18 deletions(-) diff --git a/src/main/resources/static/js/fileInput.js b/src/main/resources/static/js/fileInput.js index 87454277..d9affafc 100644 --- a/src/main/resources/static/js/fileInput.js +++ b/src/main/resources/static/js/fileInput.js @@ -1,30 +1,83 @@ document.addEventListener('DOMContentLoaded', function() { - const fileInput = document.getElementById(elementID); - // Prevent default behavior for drag events - ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { - fileInput.addEventListener(eventName, preventDefaults, false); - }); - function preventDefaults(e) { - e.preventDefault(); - e.stopPropagation(); - } + let overlay; + let dragCounter = 0; - // Add drop event listener - fileInput.addEventListener('drop', handleDrop, false); + const dragenterListener = function() { + dragCounter++; + if (!overlay) { + // Create and show the overlay + overlay = document.createElement('div'); + overlay.style.position = 'fixed'; + overlay.style.top = 0; + overlay.style.left = 0; + overlay.style.width = '100%'; + overlay.style.height = '100%'; + overlay.style.background = 'rgba(0, 0, 0, 0.5)'; + overlay.style.color = '#fff'; + overlay.style.zIndex = '1000'; + overlay.style.display = 'flex'; + overlay.style.alignItems = 'center'; + overlay.style.justifyContent = 'center'; + overlay.style.pointerEvents = 'none'; + overlay.innerHTML = '

Drop files anywhere to upload

'; + document.getElementById('content-wrap').appendChild(overlay); + } + }; + + const dragleaveListener = function() { + dragCounter--; + if (dragCounter === 0) { + // Hide and remove the overlay + if (overlay) { + overlay.remove(); + overlay = null; + } + } + }; + + const dropListener = function(e) { + const dt = e.dataTransfer; + const files = dt.files; + + // Access the file input element and assign dropped files + const fileInput = document.getElementById(elementID); + fileInput.files = files; + + // Hide and remove the overlay + if (overlay) { + overlay.remove(); + overlay = null; + } + + // Reset drag counter + dragCounter = 0; + + handleFileInputChange(fileInput); + }; + + // Prevent default behavior for drag events + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + document.body.addEventListener(eventName, preventDefaults, false); + }); + + function preventDefaults(e) { + e.preventDefault(); + e.stopPropagation(); + } + + document.body.addEventListener('dragenter', dragenterListener); + document.body.addEventListener('dragleave', dragleaveListener); + // Add drop event listener + document.body.addEventListener('drop', dropListener); - function handleDrop(e) { - const dt = e.dataTransfer; - const files = dt.files; - fileInput.files = files; - handleFileInputChange(fileInput) - } }); $("#"+elementID).on("change", function() { - handleFileInputChange(this); + handleFileInputChange(this); }); + function handleFileInputChange(inputElement) { const files = $(inputElement).get(0).files; const fileNames = Array.from(files).map(f => f.name); From 6e32c7fe855b94504b29bd98929f0a54d8c35b0a Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 15 Jul 2023 11:39:10 +0100 Subject: [PATCH 13/26] auto split init --- build.gradle | 2 + .../api/other/AutoSplitPdfController.java | 132 ++++++++++++++++++ .../controller/web/GeneralWebController.java | 8 ++ src/main/resources/messages_en_GB.properties | 3 + .../resources/templates/auto-split-pdf.html | 31 ++++ .../resources/templates/fragments/navbar.html | 2 + src/main/resources/templates/home.html | 2 +- 7 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 src/main/java/stirling/software/SPDF/controller/api/other/AutoSplitPdfController.java create mode 100644 src/main/resources/templates/auto-split-pdf.html diff --git a/build.gradle b/build.gradle index 5342c96d..0afc366b 100644 --- a/build.gradle +++ b/build.gradle @@ -62,6 +62,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-core' + implementation group: 'com.google.zxing', name: 'core', version: '3.5.1' + developmentOnly("org.springframework.boot:spring-boot-devtools") } diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/AutoSplitPdfController.java b/src/main/java/stirling/software/SPDF/controller/api/other/AutoSplitPdfController.java new file mode 100644 index 00000000..c706cba7 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/other/AutoSplitPdfController.java @@ -0,0 +1,132 @@ +package stirling.software.SPDF.controller.api.other; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.awt.image.DataBufferInt; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.rendering.PDFRenderer; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.LuminanceSource; +import com.google.zxing.MultiFormatReader; +import com.google.zxing.NotFoundException; +import com.google.zxing.PlanarYUVLuminanceSource; +import com.google.zxing.Result; +import com.google.zxing.common.HybridBinarizer; + +import stirling.software.SPDF.utils.WebResponseUtils; + +@RestController +public class AutoSplitPdfController { + + private static final String QR_CONTENT = "https://github.com/Frooodle/Stirling-PDF"; + + @PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data") + public ResponseEntity autoSplitPdf(@RequestParam("fileInput") MultipartFile file) throws IOException { + InputStream inputStream = file.getInputStream(); + PDDocument document = PDDocument.load(inputStream); + PDFRenderer pdfRenderer = new PDFRenderer(document); + + List splitDocuments = new ArrayList<>(); + List splitDocumentsBoas = new ArrayList<>(); // create this list to store ByteArrayOutputStreams for zipping + + for (int page = 0; page < document.getNumberOfPages(); ++page) { + BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 150); + String result = decodeQRCode(bim); + + if(QR_CONTENT.equals(result) && page != 0) { + splitDocuments.add(new PDDocument()); + } + + if (!splitDocuments.isEmpty() && !QR_CONTENT.equals(result)) { + splitDocuments.get(splitDocuments.size() - 1).addPage(document.getPage(page)); + } else if (page == 0) { + PDDocument firstDocument = new PDDocument(); + firstDocument.addPage(document.getPage(page)); + splitDocuments.add(firstDocument); + } + } + + // After all pages are added to splitDocuments, convert each to ByteArrayOutputStream and add to splitDocumentsBoas + for (PDDocument splitDocument : splitDocuments) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + splitDocument.save(baos); + splitDocumentsBoas.add(baos); + splitDocument.close(); + } + + document.close(); + + // After this line, you can find your zip logic integrated + Path zipFile = Files.createTempFile("split_documents", ".zip"); + String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", ""); + byte[] data; + try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) { + // loop through the split documents and write them to the zip file + for (int i = 0; i < splitDocumentsBoas.size(); i++) { + String fileName = filename + "_" + (i + 1) + ".pdf"; // You should replace "originalFileName" with the real file name + ByteArrayOutputStream baos = splitDocumentsBoas.get(i); + byte[] pdf = baos.toByteArray(); + + // Add PDF file to the zip + ZipEntry pdfEntry = new ZipEntry(fileName); + zipOut.putNextEntry(pdfEntry); + zipOut.write(pdf); + zipOut.closeEntry(); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + data = Files.readAllBytes(zipFile); + Files.delete(zipFile); + } + + + + // return the Resource in the response + return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); + } + + + private static String decodeQRCode(BufferedImage bufferedImage) { + LuminanceSource source; + + if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte) { + byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData(); + source = new PlanarYUVLuminanceSource(pixels, bufferedImage.getWidth(), bufferedImage.getHeight(), 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight(), false); + } else if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferInt) { + int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData(); + byte[] newPixels = new byte[pixels.length]; + for (int i = 0; i < pixels.length; i++) { + newPixels[i] = (byte) (pixels[i] & 0xff); + } + source = new PlanarYUVLuminanceSource(newPixels, bufferedImage.getWidth(), bufferedImage.getHeight(), 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight(), false); + } else { + throw new IllegalArgumentException("BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed int), byte gray, or 3-byte/4-byte RGB image data"); + } + + BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); + + try { + Result result = new MultiFormatReader().decode(bitmap); + return result.getText(); + } catch (NotFoundException e) { + return null; // there is no QR code in the image + } + } +} diff --git a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java index 31687299..dcf953a5 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java @@ -143,4 +143,12 @@ public class GeneralWebController { model.addAttribute("currentPage", "crop"); return "crop"; } + + + @GetMapping("/auto-split-pdf") + @Hidden + public String autoSPlitPDFForm(Model model) { + model.addAttribute("currentPage", "auto-split-pdf"); + return "auto-split-pdf"; + } } diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 2a6941f6..0da26ea7 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -149,6 +149,9 @@ 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!) +home.autoSplitPDF.title=Auto Split Pages +home.autoSplitPDF.desc=Auto Split Scanned PDF with physical scanned page splitter QR Code + 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/templates/auto-split-pdf.html b/src/main/resources/templates/auto-split-pdf.html new file mode 100644 index 00000000..faf20168 --- /dev/null +++ b/src/main/resources/templates/auto-split-pdf.html @@ -0,0 +1,31 @@ + + + + + + + + +
+
+
+

+
+
+
+

+
+
+ + +
+ +
+
+
+ +
+
+
+ + \ 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 7b3902cc..18425f11 100644 --- a/src/main/resources/templates/fragments/navbar.html +++ b/src/main/resources/templates/fragments/navbar.html @@ -54,6 +54,8 @@
+
+ diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index 012070c6..fc3d7f7f 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -77,7 +77,7 @@
- +
From 9af1b0cfdc2e2393e67a19e495a83d723ca4270f Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 15 Jul 2023 16:06:33 +0100 Subject: [PATCH 14/26] some more changes also broke pipeline a bit --- .../api/filters/FilterController.java | 333 +++++++++--------- .../api/other/AutoSplitPdfController.java | 7 +- ...ontroller.java => PipelineController.java} | 45 ++- .../api/security/WatermarkController.java | 45 ++- .../software/SPDF/utils/PdfUtils.java | 2 +- src/main/resources/static/js/pipeline.js | 113 +++--- src/main/resources/templates/pipeline.html | 198 ++++++----- 7 files changed, 430 insertions(+), 313 deletions(-) rename src/main/java/stirling/software/SPDF/controller/api/pipeline/{Controller.java => PipelineController.java} (89%) diff --git a/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java b/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java index 13732ba3..72eefe84 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java @@ -1,162 +1,171 @@ -package stirling.software.SPDF.controller.api.filters; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; - -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import stirling.software.SPDF.utils.PdfUtils; -import stirling.software.SPDF.utils.ProcessExecutor; -import stirling.software.SPDF.utils.WebResponseUtils; - -@RestController -@Tag(name = "Filter", description = "Filter APIs") -public class FilterController { - - @PostMapping(consumes = "multipart/form-data", value = "/contains-text") - @Operation(summary = "Checks if a PDF contains set text, returns true if does", description = "Input:PDF Output:Boolean Type:SISO") - public Boolean containsText( - @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile, - @Parameter(description = "The text to check for", required = true) String text, - @Parameter(description = "The page number to check for text on accepts 'All', ranges like '1-4'", required = false) String pageNumber) - throws IOException, InterruptedException { - PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); - return PdfUtils.hasText(pdfDocument, pageNumber); - } - - @PostMapping(consumes = "multipart/form-data", value = "/contains-image") - @Operation(summary = "Checks if a PDF contains an image", description = "Input:PDF Output:Boolean Type:SISO") - public Boolean containsImage( - @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile, - @Parameter(description = "The page number to check for image on accepts 'All', ranges like '1-4'", required = false) String pageNumber) - throws IOException, InterruptedException { - PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); - return PdfUtils.hasImagesOnPage(null); - } - - @PostMapping(consumes = "multipart/form-data", value = "/page-count") - @Operation(summary = "Checks if a PDF is greater, less or equal to a setPageCount", description = "Input:PDF Output:Boolean Type:SISO") - public Boolean pageCount( - @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile, - @Parameter(description = "Page Count", required = true) String pageCount, - @Parameter(description = "Comparison type, accepts Greater, Equal, Less than", required = false) String comparator) - throws IOException, InterruptedException { - // Load the PDF - PDDocument document = PDDocument.load(inputFile.getInputStream()); - int actualPageCount = document.getNumberOfPages(); - - // Perform the comparison - switch (comparator) { - case "Greater": - return actualPageCount > Integer.parseInt(pageCount); - case "Equal": - return actualPageCount == Integer.parseInt(pageCount); - case "Less": - return actualPageCount < Integer.parseInt(pageCount); - default: - throw new IllegalArgumentException("Invalid comparator: " + comparator); - } - } - - @PostMapping(consumes = "multipart/form-data", value = "/page-size") - @Operation(summary = "Checks if a PDF is of a certain size", description = "Input:PDF Output:Boolean Type:SISO") - public Boolean pageSize( - @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile, - @Parameter(description = "Standard Page Size", required = true) String standardPageSize, - @Parameter(description = "Comparison type, accepts Greater, Equal, Less than", required = false) String comparator) - throws IOException, InterruptedException { - - // Load the PDF - PDDocument document = PDDocument.load(inputFile.getInputStream()); - - PDPage firstPage = document.getPage(0); - PDRectangle actualPageSize = firstPage.getMediaBox(); - - // Calculate the area of the actual page size - float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight(); - - // Get the standard size and calculate its area - PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize); - float standardArea = standardSize.getWidth() * standardSize.getHeight(); - - // Perform the comparison - switch (comparator) { - case "Greater": - return actualArea > standardArea; - case "Equal": - return actualArea == standardArea; - case "Less": - return actualArea < standardArea; - default: - throw new IllegalArgumentException("Invalid comparator: " + comparator); - } - } - - - @PostMapping(consumes = "multipart/form-data", value = "/file-size") - @Operation(summary = "Checks if a PDF is a set file size", description = "Input:PDF Output:Boolean Type:SISO") - public Boolean fileSize( - @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile, - @Parameter(description = "File Size", required = true) String fileSize, - @Parameter(description = "Comparison type, accepts Greater, Equal, Less than", required = false) String comparator) - throws IOException, InterruptedException { - - // Get the file size - long actualFileSize = inputFile.getSize(); - - // Perform the comparison - switch (comparator) { - case "Greater": - return actualFileSize > Long.parseLong(fileSize); - case "Equal": - return actualFileSize == Long.parseLong(fileSize); - case "Less": - return actualFileSize < Long.parseLong(fileSize); - default: - throw new IllegalArgumentException("Invalid comparator: " + comparator); - } - } - - - @PostMapping(consumes = "multipart/form-data", value = "/page-rotation") - @Operation(summary = "Checks if a PDF is of a certain rotation", description = "Input:PDF Output:Boolean Type:SISO") - public Boolean pageRotation( - @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile, - @Parameter(description = "Rotation in degrees", required = true) int rotation, - @Parameter(description = "Comparison type, accepts Greater, Equal, Less than", required = false) String comparator) - throws IOException, InterruptedException { - - // Load the PDF - PDDocument document = PDDocument.load(inputFile.getInputStream()); - - // Get the rotation of the first page - PDPage firstPage = document.getPage(0); - int actualRotation = firstPage.getRotation(); - - // Perform the comparison - switch (comparator) { - case "Greater": - return actualRotation > rotation; - case "Equal": - return actualRotation == rotation; - case "Less": - return actualRotation < rotation; - default: - throw new IllegalArgumentException("Invalid comparator: " + comparator); - } - } - -} +package stirling.software.SPDF.controller.api.filters; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import stirling.software.SPDF.utils.PdfUtils; +import stirling.software.SPDF.utils.ProcessExecutor; +import stirling.software.SPDF.utils.WebResponseUtils; +import io.swagger.v3.oas.annotations.media.Schema; +@RestController +@Tag(name = "Filter", description = "Filter APIs") +public class FilterController { + + @PostMapping(consumes = "multipart/form-data", value = "/contains-text") + @Operation(summary = "Checks if a PDF contains set text, returns true if does", description = "Input:PDF Output:Boolean Type:SISO") + public Boolean containsText( + @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile, + @Parameter(description = "The text to check for", required = true) String text, + @Parameter(description = "The page number to check for text on accepts 'All', ranges like '1-4'", required = false) String pageNumber) + throws IOException, InterruptedException { + PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); + return PdfUtils.hasText(pdfDocument, pageNumber); + } + + //TODO + @PostMapping(consumes = "multipart/form-data", value = "/contains-image") + @Operation(summary = "Checks if a PDF contains an image", description = "Input:PDF Output:Boolean Type:SISO") + public Boolean containsImage( + @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile, + @Parameter(description = "The page number to check for image on accepts 'All', ranges like '1-4'", required = false) String pageNumber) + throws IOException, InterruptedException { + PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); + return PdfUtils.hasImagesOnPage(null); + } + + @PostMapping(consumes = "multipart/form-data", value = "/page-count") + @Operation(summary = "Checks if a PDF is greater, less or equal to a setPageCount", description = "Input:PDF Output:Boolean Type:SISO") + public Boolean pageCount( + @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile, + @Parameter(description = "Page Count", required = true) String pageCount, + @Parameter(description = "Comparison type", + schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", + allowableValues = {"Greater", "Equal", "Less"})) String comparator) + throws IOException, InterruptedException { + // Load the PDF + PDDocument document = PDDocument.load(inputFile.getInputStream()); + int actualPageCount = document.getNumberOfPages(); + + // Perform the comparison + switch (comparator) { + case "Greater": + return actualPageCount > Integer.parseInt(pageCount); + case "Equal": + return actualPageCount == Integer.parseInt(pageCount); + case "Less": + return actualPageCount < Integer.parseInt(pageCount); + default: + throw new IllegalArgumentException("Invalid comparator: " + comparator); + } + } + + @PostMapping(consumes = "multipart/form-data", value = "/page-size") + @Operation(summary = "Checks if a PDF is of a certain size", description = "Input:PDF Output:Boolean Type:SISO") + public Boolean pageSize( + @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile, + @Parameter(description = "Standard Page Size", required = true) String standardPageSize, + @Parameter(description = "Comparison type", + schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", + allowableValues = {"Greater", "Equal", "Less"})) String comparator) + throws IOException, InterruptedException { + + // Load the PDF + PDDocument document = PDDocument.load(inputFile.getInputStream()); + + PDPage firstPage = document.getPage(0); + PDRectangle actualPageSize = firstPage.getMediaBox(); + + // Calculate the area of the actual page size + float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight(); + + // Get the standard size and calculate its area + PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize); + float standardArea = standardSize.getWidth() * standardSize.getHeight(); + + // Perform the comparison + switch (comparator) { + case "Greater": + return actualArea > standardArea; + case "Equal": + return actualArea == standardArea; + case "Less": + return actualArea < standardArea; + default: + throw new IllegalArgumentException("Invalid comparator: " + comparator); + } + } + + + @PostMapping(consumes = "multipart/form-data", value = "/file-size") + @Operation(summary = "Checks if a PDF is a set file size", description = "Input:PDF Output:Boolean Type:SISO") + public Boolean fileSize( + @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile, + @Parameter(description = "File Size", required = true) String fileSize, + @Parameter(description = "Comparison type", + schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", + allowableValues = {"Greater", "Equal", "Less"})) String comparator) + throws IOException, InterruptedException { + + // Get the file size + long actualFileSize = inputFile.getSize(); + + // Perform the comparison + switch (comparator) { + case "Greater": + return actualFileSize > Long.parseLong(fileSize); + case "Equal": + return actualFileSize == Long.parseLong(fileSize); + case "Less": + return actualFileSize < Long.parseLong(fileSize); + default: + throw new IllegalArgumentException("Invalid comparator: " + comparator); + } + } + + + @PostMapping(consumes = "multipart/form-data", value = "/page-rotation") + @Operation(summary = "Checks if a PDF is of a certain rotation", description = "Input:PDF Output:Boolean Type:SISO") + public Boolean pageRotation( + @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile, + @Parameter(description = "Rotation in degrees", required = true) int rotation, + @Parameter(description = "Comparison type", + schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", + allowableValues = {"Greater", "Equal", "Less"})) String comparator) + throws IOException, InterruptedException { + + // Load the PDF + PDDocument document = PDDocument.load(inputFile.getInputStream()); + + // Get the rotation of the first page + PDPage firstPage = document.getPage(0); + int actualRotation = firstPage.getRotation(); + + // Perform the comparison + switch (comparator) { + case "Greater": + return actualRotation > rotation; + case "Equal": + return actualRotation == rotation; + case "Less": + return actualRotation < rotation; + default: + throw new IllegalArgumentException("Invalid comparator: " + comparator); + } + } + +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/AutoSplitPdfController.java b/src/main/java/stirling/software/SPDF/controller/api/other/AutoSplitPdfController.java index c706cba7..e69c0597 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/AutoSplitPdfController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/AutoSplitPdfController.java @@ -30,6 +30,8 @@ import com.google.zxing.Result; import com.google.zxing.common.HybridBinarizer; import stirling.software.SPDF.utils.WebResponseUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; @RestController public class AutoSplitPdfController { @@ -37,7 +39,10 @@ public class AutoSplitPdfController { private static final String QR_CONTENT = "https://github.com/Frooodle/Stirling-PDF"; @PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data") - public ResponseEntity autoSplitPdf(@RequestParam("fileInput") MultipartFile file) 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 Type:SISO") + public ResponseEntity autoSplitPdf( + @RequestParam("fileInput") @Parameter(description = "The input PDF file which needs to be split into separate documents based on QR code boundaries.", required = true) MultipartFile file) + throws IOException { InputStream inputStream = file.getInputStream(); PDDocument document = PDDocument.load(inputStream); PDFRenderer pdfRenderer = new PDFRenderer(document); diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/Controller.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java similarity index 89% rename from src/main/java/stirling/software/SPDF/controller/api/pipeline/Controller.java rename to src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java index 8193e0c8..feeefbd0 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/Controller.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java @@ -53,9 +53,9 @@ import stirling.software.SPDF.utils.WebResponseUtils; @RestController @Tag(name = "Pipeline", description = "Pipeline APIs") -public class Controller { +public class PipelineController { - private static final Logger logger = LoggerFactory.getLogger(Controller.class); + private static final Logger logger = LoggerFactory.getLogger(PipelineController.class); @Autowired private ObjectMapper objectMapper; @@ -246,11 +246,11 @@ public class Controller { List processFiles(List outputFiles, String jsonString) throws Exception { - logger.info("Processing files... " + outputFiles); ObjectMapper mapper = new ObjectMapper(); JsonNode jsonNode = mapper.readTree(jsonString); JsonNode pipelineNode = jsonNode.get("pipeline"); + logger.info("Running pipelineNode: {}", pipelineNode); ByteArrayOutputStream logStream = new ByteArrayOutputStream(); PrintStream logPrintStream = new PrintStream(logStream); @@ -298,19 +298,32 @@ public class Controller { 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); - } + + // Define filename + String filename; + if ("auto-rename".equals(operation)) { + // If the operation is "auto-rename", generate a new filename. + // This is a simple example of generating a filename using current timestamp. + // Modify as per your needs. + filename = "file_" + System.currentTimeMillis(); + } else { + // Otherwise, keep the original filename. + filename = file.getFilename(); + } + + // Check if the response body is a zip file + if (isZip(response.getBody())) { + // Unzip the file and add all the files to the new output files + newOutputFiles.addAll(unzip(response.getBody())); + } else { + Resource outputResource = new ByteArrayResource(response.getBody()) { + @Override + public String getFilename() { + return filename; + } + }; + newOutputFiles.add(outputResource); + } } if (!hasInputFileType) { 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 962f578e..a8271207 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java @@ -47,6 +47,11 @@ public class WatermarkController { @RequestPart(required = true) @Parameter(description = "The watermark type (text or image)") String watermarkType, @RequestPart(required = false) @Parameter(description = "The watermark text") String watermarkText, @RequestPart(required = false) @Parameter(description = "The watermark image") MultipartFile watermarkImage, + + @RequestParam(defaultValue = "roman", name = "alphabet") @Parameter(description = "The selected alphabet", + schema = @Schema(type = "string", + allowableValues = {"roman","arabic","japanese","korean","chinese"}, + defaultValue = "roman")) String alphabet, @RequestParam(defaultValue = "30", name = "fontSize") @Parameter(description = "The font size of the watermark text", example = "30") float fontSize, @RequestParam(defaultValue = "0", name = "rotation") @Parameter(description = "The rotation of the watermark in degrees", example = "0") float rotation, @RequestParam(defaultValue = "0.5", name = "opacity") @Parameter(description = "The opacity of the watermark (0.0 - 1.0)", example = "0.5") float opacity, @@ -71,7 +76,7 @@ public class WatermarkController { if (watermarkType.equalsIgnoreCase("text")) { addTextWatermark(contentStream, watermarkText, document, page, rotation, widthSpacer, heightSpacer, - fontSize); + fontSize, alphabet); } else if (watermarkType.equalsIgnoreCase("image")) { addImageWatermark(contentStream, watermarkImage, document, page, rotation, widthSpacer, heightSpacer, fontSize); @@ -86,9 +91,41 @@ public class WatermarkController { } private void addTextWatermark(PDPageContentStream contentStream, String watermarkText, PDDocument document, - PDPage page, float rotation, int widthSpacer, int heightSpacer, float fontSize) throws IOException { - // Set font and other properties for text watermark - PDFont font = PDType1Font.HELVETICA_BOLD; + PDPage page, float rotation, int widthSpacer, int heightSpacer, float fontSize, String alphabet) throws IOException { + String resourceDir = ""; + PDFont font = PDType1Font.HELVETICA_BOLD; + switch (alphabet) { + case "arabic": + resourceDir = "static/fonts/NotoSansArabic-Regular.ttf"; + break; + case "japanese": + resourceDir = "static/fonts/Meiryo.ttf"; + break; + case "korean": + resourceDir = "static/fonts/malgun.ttf"; + break; + case "chinese": + resourceDir = "static/fonts/SimSun.ttf"; + break; + case "roman": + default: + resourceDir = "static/fonts/NotoSans-Regular.ttf"; + break; + } + + + if(!resourceDir.equals("")) { + ClassPathResource classPathResource = new ClassPathResource(resourceDir); + String fileExtension = resourceDir.substring(resourceDir.lastIndexOf(".")); + File tempFile = File.createTempFile("NotoSansFont", fileExtension); + try (InputStream is = classPathResource.getInputStream(); FileOutputStream os = new FileOutputStream(tempFile)) { + IOUtils.copy(is, os); + } + + font = PDType0Font.load(document, tempFile); + tempFile.deleteOnExit(); + } + contentStream.setFont(font, fontSize); contentStream.setNonStrokingColor(Color.LIGHT_GRAY); diff --git a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java index 5e9d53b0..43e26017 100644 --- a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java @@ -44,7 +44,7 @@ public class PdfUtils { public static PDRectangle textToPageSize(String size) { - switch (size) { + switch (size.toUpperCase()) { case "A0": return PDRectangle.A0; case "A1": diff --git a/src/main/resources/static/js/pipeline.js b/src/main/resources/static/js/pipeline.js index 85918cd5..c2c4601a 100644 --- a/src/main/resources/static/js/pipeline.js +++ b/src/main/resources/static/js/pipeline.js @@ -87,7 +87,7 @@ document.getElementById('submitConfigBtn').addEventListener('click', function() let formData = new FormData(); - let fileInput = document.getElementById('fileInput'); + let fileInput = document.getElementById('fileInput-input'); let files = fileInput.files; for (let i = 0; i < files.length; i++) { @@ -177,7 +177,11 @@ document.getElementById('addOperationBtn').addEventListener('click', function() let listItem = document.createElement('li'); listItem.className = "list-group-item"; let hasSettings = (apiDocs[selectedOperation] && apiDocs[selectedOperation].post && - apiDocs[selectedOperation].post.parameters && apiDocs[selectedOperation].post.parameters.length > 0); + ((apiDocs[selectedOperation].post.parameters && apiDocs[selectedOperation].post.parameters.length > 0) || + (apiDocs[selectedOperation].post.requestBody && + apiDocs[selectedOperation].post.requestBody.content['multipart/form-data'].schema.properties))); + + listItem.innerHTML = ` @@ -222,52 +226,77 @@ document.getElementById('addOperationBtn').addEventListener('click', function() let pipelineSettingsModal = document.getElementById('pipelineSettingsModal'); let pipelineSettingsContent = document.getElementById('pipelineSettingsContent'); let operationData = apiDocs[operation].post.parameters || []; + let requestBodyData = apiDocs[operation].post.requestBody.content['multipart/form-data'].schema.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 => { - let parameterDiv = document.createElement('div'); - parameterDiv.className = "form-group"; + let parameterDiv = document.createElement('div'); + parameterDiv.className = "form-group"; - let parameterLabel = document.createElement('label'); - parameterLabel.textContent = `${parameter.name} (${parameter.schema.type}): `; - parameterLabel.title = parameter.description; - parameterDiv.appendChild(parameterLabel); + let parameterLabel = document.createElement('label'); + parameterLabel.textContent = `${parameter.name} (${parameter.schema.type}): `; + parameterLabel.title = parameter.description; + parameterDiv.appendChild(parameterLabel); - let parameterInput; - switch (parameter.schema.type) { - case 'string': - case 'number': - case 'integer': - parameterInput = document.createElement('input'); - parameterInput.type = parameter.schema.type === 'string' ? 'text' : 'number'; - parameterInput.className = "form-control"; - break; - case 'boolean': - parameterInput = document.createElement('input'); - parameterInput.type = 'checkbox'; - break; - case 'array': - case 'object': - parameterInput = document.createElement('textarea'); - parameterInput.placeholder = `Enter a JSON formatted ${parameter.schema.type}`; - parameterInput.className = "form-control"; - break; - case 'enum': - parameterInput = document.createElement('select'); - parameterInput.className = "form-control"; - parameter.schema.enum.forEach(option => { - let optionElement = document.createElement('option'); - optionElement.value = option; - optionElement.text = option; - parameterInput.appendChild(optionElement); - }); - break; - default: - parameterInput = document.createElement('input'); - parameterInput.type = 'text'; - parameterInput.className = "form-control"; - } + 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"; + } else { + parameterInput = document.createElement('input'); + parameterInput.type = 'text'; + parameterInput.className = "form-control"; + } + break; + case 'number': + case 'integer': + parameterInput = document.createElement('input'); + parameterInput.type = 'number'; + parameterInput.className = "form-control"; + break; + case 'boolean': + parameterInput = document.createElement('input'); + parameterInput.type = 'checkbox'; + 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"; + } + } parameterInput.id = parameter.name; if (operationSettings[operation] && operationSettings[operation][parameter.name] !== undefined) { diff --git a/src/main/resources/templates/pipeline.html b/src/main/resources/templates/pipeline.html index dc959a39..1f61008d 100644 --- a/src/main/resources/templates/pipeline.html +++ b/src/main/resources/templates/pipeline.html @@ -3,98 +3,122 @@ th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> - - -
-
-
-

-
-
- - - - - - - - - - - - - - - - - - - - - -
-
-
- + + +
+
+
+ +

+
+
+ +
+
+ + +
+ +
+
+ +
+
+
+
+
+ +
+
+
+ + + + + + + +
+
+
+
From 29aabdfba86f5240fbf110547112a9211501eb8e Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sun, 16 Jul 2023 00:36:58 +0100 Subject: [PATCH 15/26] filter --- .../api/filters/FilterController.java | 141 ++++++----- .../api/pipeline/PipelineController.java | 8 +- .../software/SPDF/utils/GeneralUtils.java | 220 +++++++++--------- .../software/SPDF/utils/PdfUtils.java | 71 +++--- .../software/SPDF/utils/WebResponseUtils.java | 111 +++++---- 5 files changed, 302 insertions(+), 249 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java b/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java index 72eefe84..3409e1d3 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java @@ -22,68 +22,79 @@ import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.WebResponseUtils; import io.swagger.v3.oas.annotations.media.Schema; + @RestController @Tag(name = "Filter", description = "Filter APIs") public class FilterController { - @PostMapping(consumes = "multipart/form-data", value = "/contains-text") + @PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text") @Operation(summary = "Checks if a PDF contains set text, returns true if does", description = "Input:PDF Output:Boolean Type:SISO") - public Boolean containsText( + public ResponseEntity containsText( @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile, @Parameter(description = "The text to check for", required = true) String text, @Parameter(description = "The page number to check for text on accepts 'All', ranges like '1-4'", required = false) String pageNumber) throws IOException, InterruptedException { PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); - return PdfUtils.hasText(pdfDocument, pageNumber); + if (PdfUtils.hasText(pdfDocument, pageNumber, text)) + return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename()); + return null; } - //TODO - @PostMapping(consumes = "multipart/form-data", value = "/contains-image") + // 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 Boolean containsImage( + public ResponseEntity containsImage( @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) MultipartFile inputFile, @Parameter(description = "The page number to check for image on accepts 'All', ranges like '1-4'", required = false) String pageNumber) throws IOException, InterruptedException { PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); - return PdfUtils.hasImagesOnPage(null); + if (PdfUtils.hasImages(pdfDocument, pageNumber)) + return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename()); + return null; } - @PostMapping(consumes = "multipart/form-data", value = "/page-count") + @PostMapping(consumes = "multipart/form-data", value = "/filter-page-count") @Operation(summary = "Checks if a PDF is greater, less or equal to a setPageCount", description = "Input:PDF Output:Boolean Type:SISO") - public Boolean pageCount( + public ResponseEntity pageCount( @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile, @Parameter(description = "Page Count", required = true) String pageCount, - @Parameter(description = "Comparison type", - schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", - allowableValues = {"Greater", "Equal", "Less"})) String comparator) + @Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = { + "Greater", "Equal", "Less" })) String comparator) throws IOException, InterruptedException { // Load the PDF PDDocument document = PDDocument.load(inputFile.getInputStream()); int actualPageCount = document.getNumberOfPages(); + boolean valid = false; // Perform the comparison switch (comparator) { case "Greater": - return actualPageCount > Integer.parseInt(pageCount); + valid = actualPageCount > Integer.parseInt(pageCount); + break; case "Equal": - return actualPageCount == Integer.parseInt(pageCount); + valid = actualPageCount == Integer.parseInt(pageCount); + break; case "Less": - return actualPageCount < Integer.parseInt(pageCount); + valid = actualPageCount < Integer.parseInt(pageCount); + break; default: throw new IllegalArgumentException("Invalid comparator: " + comparator); } + + if (valid) + return WebResponseUtils.multiPartFileToWebResponse(inputFile); + return null; } - @PostMapping(consumes = "multipart/form-data", value = "/page-size") + @PostMapping(consumes = "multipart/form-data", value = "/filter-page-size") @Operation(summary = "Checks if a PDF is of a certain size", description = "Input:PDF Output:Boolean Type:SISO") - public Boolean pageSize( - @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile, - @Parameter(description = "Standard Page Size", required = true) String standardPageSize, - @Parameter(description = "Comparison type", - schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", - allowableValues = {"Greater", "Equal", "Less"})) String comparator) - throws IOException, InterruptedException { - + public ResponseEntity pageSize( + @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile, + @Parameter(description = "Standard Page Size", required = true) String standardPageSize, + @Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = { + "Greater", "Equal", "Less" })) String comparator) + throws IOException, InterruptedException { + // Load the PDF PDDocument document = PDDocument.load(inputFile.getInputStream()); @@ -97,75 +108,95 @@ public class FilterController { PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize); float standardArea = standardSize.getWidth() * standardSize.getHeight(); + boolean valid = false; // Perform the comparison switch (comparator) { case "Greater": - return actualArea > standardArea; + valid = actualArea > standardArea; + break; case "Equal": - return actualArea == standardArea; + valid = actualArea == standardArea; + break; case "Less": - return actualArea < standardArea; + valid = actualArea < standardArea; + break; default: throw new IllegalArgumentException("Invalid comparator: " + comparator); } - } - - @PostMapping(consumes = "multipart/form-data", value = "/file-size") + if (valid) + return WebResponseUtils.multiPartFileToWebResponse(inputFile); + return null; + } + + @PostMapping(consumes = "multipart/form-data", value = "/filter-file-size") @Operation(summary = "Checks if a PDF is a set file size", description = "Input:PDF Output:Boolean Type:SISO") - public Boolean fileSize( - @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile, - @Parameter(description = "File Size", required = true) String fileSize, - @Parameter(description = "Comparison type", - schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", - allowableValues = {"Greater", "Equal", "Less"})) String comparator) - throws IOException, InterruptedException { - + public ResponseEntity fileSize( + @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile, + @Parameter(description = "File Size", required = true) String fileSize, + @Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = { + "Greater", "Equal", "Less" })) String comparator) + throws IOException, InterruptedException { + // Get the file size long actualFileSize = inputFile.getSize(); + boolean valid = false; // Perform the comparison switch (comparator) { case "Greater": - return actualFileSize > Long.parseLong(fileSize); + valid = actualFileSize > Long.parseLong(fileSize); + break; case "Equal": - return actualFileSize == Long.parseLong(fileSize); + valid = actualFileSize == Long.parseLong(fileSize); + break; case "Less": - return actualFileSize < Long.parseLong(fileSize); + valid = actualFileSize < Long.parseLong(fileSize); + break; default: throw new IllegalArgumentException("Invalid comparator: " + comparator); } + + if (valid) + return WebResponseUtils.multiPartFileToWebResponse(inputFile); + return null; } - - @PostMapping(consumes = "multipart/form-data", value = "/page-rotation") + @PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation") @Operation(summary = "Checks if a PDF is of a certain rotation", description = "Input:PDF Output:Boolean Type:SISO") - public Boolean pageRotation( - @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile, - @Parameter(description = "Rotation in degrees", required = true) int rotation, - @Parameter(description = "Comparison type", - schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", - allowableValues = {"Greater", "Equal", "Less"})) String comparator) - throws IOException, InterruptedException { - + public ResponseEntity pageRotation( + @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file", required = true) MultipartFile inputFile, + @Parameter(description = "Rotation in degrees", required = true) int rotation, + @Parameter(description = "Comparison type", schema = @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = { + "Greater", "Equal", "Less" })) String comparator) + throws IOException, InterruptedException { + // Load the PDF PDDocument document = PDDocument.load(inputFile.getInputStream()); // Get the rotation of the first page PDPage firstPage = document.getPage(0); int actualRotation = firstPage.getRotation(); - + boolean valid = false; // Perform the comparison switch (comparator) { case "Greater": - return actualRotation > rotation; + valid = actualRotation > rotation; + break; case "Equal": - return actualRotation == rotation; + valid = actualRotation == rotation; + break; case "Less": - return actualRotation < rotation; + 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/pipeline/PipelineController.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java index feeefbd0..244cb807 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 @@ -292,12 +292,18 @@ public class PipelineController { ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class); + // If the operation is filter and the response body is null or empty, skip this file + if (operation.startsWith("filter-") && (response.getBody() == null || response.getBody().length == 0)) { + logger.info("Skipping file due to failing {}", operation); + continue; + } + if (!response.getStatusCode().equals(HttpStatus.OK)) { logPrintStream.println("Error: " + response.getBody()); hasErrors = true; continue; } - + // Define filename String filename; diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java index c2e5aaf6..03eccf88 100644 --- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java @@ -1,107 +1,113 @@ -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; - -public class GeneralUtils { - - 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 { - // Input string does not have a valid format, handle this case - } - } catch (NumberFormatException e) { - // The numeric part of the input string cannot be parsed, handle this case - } - - return null; - } - - public static List parsePageList(String[] pageOrderArr, int totalPages) { - List newPageOrder = new ArrayList<>(); - - // loop through the page order array - for (String element : pageOrderArr) { - // check if the element contains a range of pages - 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; - } -} +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; + +public class GeneralUtils { + + 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 { + // Input string does not have a valid format, handle this case + } + } catch (NumberFormatException e) { + // The numeric part of the input string cannot be parsed, handle this case + } + + return null; + } + + public static List parsePageList(String[] pageOrderArr, int totalPages) { + List newPageOrder = new ArrayList<>(); + + // 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/PdfUtils.java b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java index 43e26017..5b116a93 100644 --- a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java @@ -68,43 +68,37 @@ public class PdfUtils { } } - public boolean hasImageInFile(PDDocument pdfDocument, String text, String pagesToCheck) throws IOException { - PDFTextStripper textStripper = new PDFTextStripper(); - String pdfText = ""; - if(pagesToCheck == null || pagesToCheck.equals("all")) { - pdfText = textStripper.getText(pdfDocument); - } else { - // remove whitespaces - pagesToCheck = pagesToCheck.replaceAll("\\s+", ""); + + + public static boolean hasImages(PDDocument document, String pagesToCheck) throws IOException { + String[] pageOrderArr = pagesToCheck.split(","); + List pageList = GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); - String[] splitPoints = pagesToCheck.split(","); - for (String splitPoint : splitPoints) { - if (splitPoint.contains("-")) { - // Handle page ranges - String[] range = splitPoint.split("-"); - int startPage = Integer.parseInt(range[0]); - int endPage = Integer.parseInt(range[1]); - - for (int i = startPage; i <= endPage; i++) { - textStripper.setStartPage(i); - textStripper.setEndPage(i); - pdfText += textStripper.getText(pdfDocument); - } - } else { - // Handle individual page - int page = Integer.parseInt(splitPoint); - textStripper.setStartPage(page); - textStripper.setEndPage(page); - pdfText += textStripper.getText(pdfDocument); - } + for (int pageNumber : pageList) { + PDPage page = document.getPage(pageNumber); + if (hasImagesOnPage(page)) { + return true; } } - pdfDocument.close(); - - return pdfText.contains(text); + return false; } + + public static boolean hasText(PDDocument document, String pageNumbersToCheck, String phrase) throws IOException { + String[] pageOrderArr = pageNumbersToCheck.split(","); + List pageList = GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); + + for (int pageNumber : pageList) { + PDPage page = document.getPage(pageNumber); + if (hasTextOnPage(page, phrase)) { + return true; + } + } + + return false; + } + public static boolean hasImagesOnPage(PDPage page) throws IOException { ImageFinder imageFinder = new ImageFinder(page); @@ -113,12 +107,17 @@ public class PdfUtils { } - public static boolean hasText(PDDocument document, String phrase) throws IOException { - PDFTextStripper pdfStripper = new PDFTextStripper(); - String text = pdfStripper.getText(document); - return text.contains(phrase); - } + + public static boolean hasTextOnPage(PDPage page, String phrase) throws IOException { + PDFTextStripper textStripper = new PDFTextStripper(); + PDDocument tempDoc = new PDDocument(); + tempDoc.addPage(page); + String pageText = textStripper.getText(tempDoc); + tempDoc.close(); + return pageText.contains(phrase); + } + public boolean containsTextInFile(PDDocument pdfDocument, String text, String pagesToCheck) throws IOException { PDFTextStripper textStripper = new PDFTextStripper(); diff --git a/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java b/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java index c986f220..59c0b056 100644 --- a/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java @@ -1,50 +1,61 @@ -package stirling.software.SPDF.utils; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; - -import org.apache.pdfbox.pdmodel.PDDocument; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; - -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, MediaType mediaType) throws IOException { - return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName, 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) 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); - } - -} +package stirling.software.SPDF.utils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +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, 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()); + + byte[] bytes = file.getBytes(); + + 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) 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); + } + +} From d07e3e6522e539675372dace00571403133f1f54 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sun, 16 Jul 2023 16:07:08 +0100 Subject: [PATCH 16/26] change add numbers grid and remove files from pipelines --- src/main/resources/static/js/pipeline.js | 317 +++++++++--------- .../templates/other/add-page-numbers.html | 283 +++++++++------- 2 files changed, 322 insertions(+), 278 deletions(-) diff --git a/src/main/resources/static/js/pipeline.js b/src/main/resources/static/js/pipeline.js index c2c4601a..8b2c263d 100644 --- a/src/main/resources/static/js/pipeline.js +++ b/src/main/resources/static/js/pipeline.js @@ -75,12 +75,12 @@ document.getElementById('submitConfigBtn').addEventListener('click', function() "operation": selectedOperation, "parameters": parameters }], - "_examples": { - "outputDir" : "{outputFolder}/{folderName}", - "outputFileName" : "{filename}-{pipelineName}-{date}-{time}" - }, - "outputDir" : "httpWebRequest", - "outputFileName" : "{filename}" + "_examples": { + "outputDir": "{outputFolder}/{folderName}", + "outputFileName": "{filename}-{pipelineName}-{date}-{time}" + }, + "outputDir": "httpWebRequest", + "outputFileName": "{filename}" }; let pipelineConfigJson = JSON.stringify(pipelineConfig, null, 2); @@ -126,11 +126,11 @@ let operationSettings = {}; fetch('v3/api-docs') .then(response => response.json()) .then(data => { - + apiDocs = data.paths; let operationsDropdown = document.getElementById('operationsDropdown'); const ignoreOperations = ["/handleData", "operationToIgnore"]; // Add the operations you want to ignore here - + operationsDropdown.innerHTML = ''; let operationsByTag = {}; @@ -148,25 +148,25 @@ fetch('v3/api-docs') }); // Specify the order of tags - let tagOrder = ["General", "Security", "Convert", "Other", "Filter"]; + let tagOrder = ["General", "Security", "Convert", "Other", "Filter"]; - // Create dropdown options - tagOrder.forEach(tag => { - if (operationsByTag[tag]) { - let group = document.createElement('optgroup'); - group.label = tag; + // 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 operationWithoutSlash = operationPath.replace(/\//g, ''); // Remove slashes - option.textContent = operationWithoutSlash; - option.value = operationPath; // Keep the value with slashes for querying - group.appendChild(option); - }); + operationsByTag[tag].forEach(operationPath => { + let option = document.createElement('option'); + let operationWithoutSlash = operationPath.replace(/\//g, ''); // Remove slashes + option.textContent = operationWithoutSlash; + option.value = operationPath; // Keep the value with slashes for querying + group.appendChild(option); + }); - operationsDropdown.appendChild(group); - } - }); + operationsDropdown.appendChild(group); + } + }); }); @@ -177,9 +177,9 @@ document.getElementById('addOperationBtn').addEventListener('click', function() let listItem = document.createElement('li'); listItem.className = "list-group-item"; let hasSettings = (apiDocs[selectedOperation] && apiDocs[selectedOperation].post && - ((apiDocs[selectedOperation].post.parameters && apiDocs[selectedOperation].post.parameters.length > 0) || - (apiDocs[selectedOperation].post.requestBody && - apiDocs[selectedOperation].post.requestBody.content['multipart/form-data'].schema.properties))); + ((apiDocs[selectedOperation].post.parameters && apiDocs[selectedOperation].post.parameters.length > 0) || + (apiDocs[selectedOperation].post.requestBody && + apiDocs[selectedOperation].post.requestBody.content['multipart/form-data'].schema.properties))); @@ -226,77 +226,86 @@ document.getElementById('addOperationBtn').addEventListener('click', function() let pipelineSettingsModal = document.getElementById('pipelineSettingsModal'); let pipelineSettingsContent = document.getElementById('pipelineSettingsContent'); let operationData = apiDocs[operation].post.parameters || []; - let requestBodyData = apiDocs[operation].post.requestBody.content['multipart/form-data'].schema.properties || {}; - - // Combine operationData and requestBodyData into a single array - operationData = operationData.concat(Object.keys(requestBodyData).map(key => ({ - name: key, - schema: requestBodyData[key] - }))); + let requestBodyData = apiDocs[operation].post.requestBody.content['multipart/form-data'].schema.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 => { - let parameterDiv = document.createElement('div'); - parameterDiv.className = "form-group"; + // 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 = "form-group"; - let parameterLabel = document.createElement('label'); - parameterLabel.textContent = `${parameter.name} (${parameter.schema.type}): `; - parameterLabel.title = parameter.description; - parameterDiv.appendChild(parameterLabel); + let parameterLabel = document.createElement('label'); + parameterLabel.textContent = `${parameter.name} (${parameter.schema.type}): `; + parameterLabel.title = parameter.description; + parameterDiv.appendChild(parameterLabel); - let parameterInput; + 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"; + // 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"; - } else { - parameterInput = document.createElement('input'); - parameterInput.type = 'text'; - parameterInput.className = "form-control"; - } - break; - case 'number': - case 'integer': - parameterInput = document.createElement('input'); - parameterInput.type = 'number'; - parameterInput.className = "form-control"; - break; - case 'boolean': - parameterInput = document.createElement('input'); - parameterInput.type = 'checkbox'; - 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"; - } - } + // 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 = "automatedFileInput"; + } else { + parameterInput = document.createElement('input'); + parameterInput.type = 'text'; + parameterInput.className = "form-control"; + } + break; + case 'number': + case 'integer': + parameterInput = document.createElement('input'); + parameterInput.type = 'number'; + parameterInput.className = "form-control"; + break; + case 'boolean': + parameterInput = document.createElement('input'); + parameterInput.type = 'checkbox'; + 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"; + } + } parameterInput.id = parameter.name; if (operationSettings[operation] && operationSettings[operation][parameter.name] !== undefined) { @@ -380,12 +389,12 @@ document.getElementById('addOperationBtn').addEventListener('click', function() let pipelineConfig = { "name": pipelineName, "pipeline": [], - "_examples": { - "outputDir" : "{outputFolder}/{folderName}", - "outputFileName" : "{filename}-{pipelineName}-{date}-{time}" - }, - "outputDir" : "httpWebRequest", - "outputFileName" : "{filename}" + "_examples": { + "outputDir": "{outputFolder}/{folderName}", + "outputFileName": "{filename}-{pipelineName}-{date}-{time}" + }, + "outputDir": "httpWebRequest", + "outputFileName": "{filename}" }; for (let i = 0; i < pipelineList.length; i++) { @@ -411,74 +420,74 @@ document.getElementById('addOperationBtn').addEventListener('click', function() }); async function processPipelineConfig(configString) { - let pipelineConfig = JSON.parse(configString); - let pipelineList = document.getElementById('pipelineList'); - - while (pipelineList.firstChild) { - pipelineList.removeChild(pipelineList.firstChild); - } - document.getElementById('pipelineName').value = pipelineConfig.name - for (const operationConfig of pipelineConfig.pipeline) { - let operationsDropdown = document.getElementById('operationsDropdown'); - operationsDropdown.value = operationConfig.operation; - operationSettings[operationConfig.operation] = operationConfig.parameters; + let pipelineConfig = JSON.parse(configString); + let pipelineList = document.getElementById('pipelineList'); - // assuming addOperation is async - await new Promise((resolve) => { - document.getElementById('addOperationBtn').addEventListener('click', resolve, { once: true }); - document.getElementById('addOperationBtn').click(); - }); + while (pipelineList.firstChild) { + pipelineList.removeChild(pipelineList.firstChild); + } + document.getElementById('pipelineName').value = pipelineConfig.name + for (const operationConfig of pipelineConfig.pipeline) { + let operationsDropdown = document.getElementById('operationsDropdown'); + operationsDropdown.value = operationConfig.operation; + operationSettings[operationConfig.operation] = operationConfig.parameters; - let lastOperation = pipelineList.lastChild; - - Object.keys(operationConfig.parameters).forEach(parameterName => { - let input = document.getElementById(parameterName); - if (input) { - switch (input.type) { - case 'checkbox': - input.checked = operationConfig.parameters[parameterName]; - break; - case 'number': - input.value = operationConfig.parameters[parameterName].toString(); - break; - case 'file': - if (parameterName !== 'fileInput') { - // Create a new file input element - let newInput = document.createElement('input'); - newInput.type = 'file'; - newInput.id = parameterName; + // assuming addOperation is async + await new Promise((resolve) => { + document.getElementById('addOperationBtn').addEventListener('click', resolve, { once: true }); + document.getElementById('addOperationBtn').click(); + }); - // Add the new file input to the main page (change the selector according to your needs) - document.querySelector('#main').appendChild(newInput); - } - break; - case 'text': - case 'textarea': - default: - input.value = JSON.stringify(operationConfig.parameters[parameterName]); - } - } - }); + let lastOperation = pipelineList.lastChild; + + Object.keys(operationConfig.parameters).forEach(parameterName => { + let input = document.getElementById(parameterName); + if (input) { + switch (input.type) { + case 'checkbox': + input.checked = operationConfig.parameters[parameterName]; + break; + case 'number': + input.value = operationConfig.parameters[parameterName].toString(); + break; + case 'file': + if (parameterName !== 'fileInput') { + // Create a new file input element + 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); + } + break; + case 'text': + case 'textarea': + default: + input.value = JSON.stringify(operationConfig.parameters[parameterName]); + } + } + }); + + } + } - } -} - document.getElementById('uploadPipelineBtn').addEventListener('click', function() { - document.getElementById('uploadPipelineInput').click(); + document.getElementById('uploadPipelineInput').click(); }); - + document.getElementById('uploadPipelineInput').addEventListener('change', function(e) { - let reader = new FileReader(); - reader.onload = function(event) { - processPipelineConfig(event.target.result); - }; - reader.readAsText(e.target.files[0]); + let reader = new FileReader(); + reader.onload = function(event) { + processPipelineConfig(event.target.result); + }; + reader.readAsText(e.target.files[0]); }); - + 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); + let selectedPipelineJson = e.target.value; // assuming the selected value is the JSON string of the pipeline config + processPipelineConfig(selectedPipelineJson); }); diff --git a/src/main/resources/templates/other/add-page-numbers.html b/src/main/resources/templates/other/add-page-numbers.html index 2ea91a3e..8ae8fe2b 100644 --- a/src/main/resources/templates/other/add-page-numbers.html +++ b/src/main/resources/templates/other/add-page-numbers.html @@ -1,125 +1,160 @@ - - - - - - - - -
-
-
-

-
-
-
-

-
-
-
-
- - -
- - - -
- -
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
- -
- - -
- - - -
- - -
-
- - -
-
- - -
- - - -
-
-
- -
-
-
- + + + + + + + + +
+
+
+

+
+
+
+

+
+
+
+
+ +
+ + + +
+ +
+
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
+ + + + +
+ + +
+ +
+
+ +
+
+ +
+ +
+ +
+
+
+ + +
+
+
+ \ No newline at end of file From 92b914290292dd1250e620e0e927d36f75727b2b Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sun, 16 Jul 2023 18:57:21 +0100 Subject: [PATCH 17/26] language cleanups and sanitize --- .../SPDF/config/EndpointConfiguration.java | 408 ++++++------ .../api/other/PageNumbersController.java | 352 +++++------ .../api/security/SanitizeController.java | 140 +++++ .../controller/web/SecurityWebController.java | 99 +-- src/main/resources/messages_en_GB.properties | 47 ++ src/main/resources/static/images/sanitize.svg | 1 + src/main/resources/templates/crop.html | 208 +++---- .../resources/templates/fragments/navbar.html | 9 +- src/main/resources/templates/home.html | 2 +- .../templates/other/add-page-numbers.html | 26 +- .../templates/other/adjust-contrast.html | 585 +++++++++--------- .../templates/other/auto-rename.html | 61 +- .../templates/security/sanitize-pdf.html | 53 ++ 13 files changed, 1120 insertions(+), 871 deletions(-) create mode 100644 src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java create mode 100644 src/main/resources/static/images/sanitize.svg create mode 100644 src/main/resources/templates/security/sanitize-pdf.html diff --git a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index 92580b43..24f2822d 100644 --- a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -1,200 +1,208 @@ -package stirling.software.SPDF.config; - -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; -@Service -public class EndpointConfiguration { - private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class); - private Map endpointStatuses = new ConcurrentHashMap<>(); - private Map> endpointGroups = new ConcurrentHashMap<>(); - - public EndpointConfiguration() { - init(); - processEnvironmentConfigs(); - } - - public void enableEndpoint(String endpoint) { - endpointStatuses.put(endpoint, true); - } - - public void disableEndpoint(String endpoint) { - if(!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) { - logger.info("Disabling {}", endpoint); - endpointStatuses.put(endpoint, false); - } - } - - public boolean isEndpointEnabled(String endpoint) { - if (endpoint.startsWith("/")) { - endpoint = endpoint.substring(1); - } - return endpointStatuses.getOrDefault(endpoint, true); - } - - public void addEndpointToGroup(String group, String endpoint) { - endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint); - } - - public void enableGroup(String group) { - Set endpoints = endpointGroups.get(group); - if (endpoints != null) { - for (String endpoint : endpoints) { - enableEndpoint(endpoint); - } - } - } - - public void disableGroup(String group) { - Set endpoints = endpointGroups.get(group); - if (endpoints != null) { - for (String endpoint : endpoints) { - disableEndpoint(endpoint); - } - } - } - - public void init() { - // Adding endpoints to "PageOps" group - addEndpointToGroup("PageOps", "remove-pages"); - addEndpointToGroup("PageOps", "merge-pdfs"); - addEndpointToGroup("PageOps", "split-pdfs"); - addEndpointToGroup("PageOps", "pdf-organizer"); - addEndpointToGroup("PageOps", "rotate-pdf"); - addEndpointToGroup("PageOps", "multi-page-layout"); - addEndpointToGroup("PageOps", "scale-pages"); - - // Adding endpoints to "Convert" group - addEndpointToGroup("Convert", "pdf-to-img"); - addEndpointToGroup("Convert", "img-to-pdf"); - addEndpointToGroup("Convert", "pdf-to-pdfa"); - addEndpointToGroup("Convert", "file-to-pdf"); - addEndpointToGroup("Convert", "xlsx-to-pdf"); - addEndpointToGroup("Convert", "pdf-to-word"); - addEndpointToGroup("Convert", "pdf-to-presentation"); - addEndpointToGroup("Convert", "pdf-to-text"); - addEndpointToGroup("Convert", "pdf-to-html"); - addEndpointToGroup("Convert", "pdf-to-xml"); - - // Adding endpoints to "Security" group - addEndpointToGroup("Security", "add-password"); - addEndpointToGroup("Security", "remove-password"); - addEndpointToGroup("Security", "change-permissions"); - addEndpointToGroup("Security", "add-watermark"); - addEndpointToGroup("Security", "cert-sign"); - - - - // Adding endpoints to "Other" group - addEndpointToGroup("Other", "ocr-pdf"); - addEndpointToGroup("Other", "add-image"); - addEndpointToGroup("Other", "compress-pdf"); - addEndpointToGroup("Other", "extract-images"); - addEndpointToGroup("Other", "change-metadata"); - addEndpointToGroup("Other", "extract-image-scans"); - addEndpointToGroup("Other", "sign"); - addEndpointToGroup("Other", "flatten"); - addEndpointToGroup("Other", "repair"); - addEndpointToGroup("Other", "remove-blanks"); - addEndpointToGroup("Other", "compare"); - - - - - - - - //CLI - addEndpointToGroup("CLI", "compress-pdf"); - addEndpointToGroup("CLI", "extract-image-scans"); - addEndpointToGroup("CLI", "remove-blanks"); - addEndpointToGroup("CLI", "repair"); - addEndpointToGroup("CLI", "pdf-to-pdfa"); - addEndpointToGroup("CLI", "file-to-pdf"); - addEndpointToGroup("CLI", "xlsx-to-pdf"); - addEndpointToGroup("CLI", "pdf-to-word"); - addEndpointToGroup("CLI", "pdf-to-presentation"); - addEndpointToGroup("CLI", "pdf-to-text"); - addEndpointToGroup("CLI", "pdf-to-html"); - addEndpointToGroup("CLI", "pdf-to-xml"); - addEndpointToGroup("CLI", "ocr-pdf"); - - //python - addEndpointToGroup("Python", "extract-image-scans"); - addEndpointToGroup("Python", "remove-blanks"); - - - - //openCV - addEndpointToGroup("OpenCV", "extract-image-scans"); - addEndpointToGroup("OpenCV", "remove-blanks"); - - //LibreOffice - addEndpointToGroup("LibreOffice", "repair"); - addEndpointToGroup("LibreOffice", "file-to-pdf"); - addEndpointToGroup("LibreOffice", "xlsx-to-pdf"); - addEndpointToGroup("LibreOffice", "pdf-to-word"); - addEndpointToGroup("LibreOffice", "pdf-to-presentation"); - addEndpointToGroup("LibreOffice", "pdf-to-text"); - addEndpointToGroup("LibreOffice", "pdf-to-html"); - addEndpointToGroup("LibreOffice", "pdf-to-xml"); - - - //OCRmyPDF - addEndpointToGroup("OCRmyPDF", "compress-pdf"); - addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa"); - addEndpointToGroup("OCRmyPDF", "ocr-pdf"); - - //Java - addEndpointToGroup("Java", "merge-pdfs"); - addEndpointToGroup("Java", "remove-pages"); - addEndpointToGroup("Java", "split-pdfs"); - addEndpointToGroup("Java", "pdf-organizer"); - addEndpointToGroup("Java", "rotate-pdf"); - addEndpointToGroup("Java", "pdf-to-img"); - addEndpointToGroup("Java", "img-to-pdf"); - addEndpointToGroup("Java", "add-password"); - addEndpointToGroup("Java", "remove-password"); - addEndpointToGroup("Java", "change-permissions"); - addEndpointToGroup("Java", "add-watermark"); - addEndpointToGroup("Java", "add-image"); - addEndpointToGroup("Java", "extract-images"); - addEndpointToGroup("Java", "change-metadata"); - addEndpointToGroup("Java", "cert-sign"); - addEndpointToGroup("Java", "multi-page-layout"); - addEndpointToGroup("Java", "scale-pages"); - - - //Javascript - addEndpointToGroup("Javascript", "pdf-organizer"); - addEndpointToGroup("Javascript", "sign"); - addEndpointToGroup("Javascript", "compare"); - - } - - private void processEnvironmentConfigs() { - String endpointsToRemove = System.getenv("ENDPOINTS_TO_REMOVE"); - String groupsToRemove = System.getenv("GROUPS_TO_REMOVE"); - - if (endpointsToRemove != null) { - String[] endpoints = endpointsToRemove.split(","); - for (String endpoint : endpoints) { - disableEndpoint(endpoint.trim()); - } - } - - if (groupsToRemove != null) { - String[] groups = groupsToRemove.split(","); - for (String group : groups) { - disableGroup(group.trim()); - } - } - } - -} - +package stirling.software.SPDF.config; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +@Service +public class EndpointConfiguration { + private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class); + private Map endpointStatuses = new ConcurrentHashMap<>(); + private Map> endpointGroups = new ConcurrentHashMap<>(); + + public EndpointConfiguration() { + init(); + processEnvironmentConfigs(); + } + + public void enableEndpoint(String endpoint) { + endpointStatuses.put(endpoint, true); + } + + public void disableEndpoint(String endpoint) { + if(!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) { + logger.info("Disabling {}", endpoint); + endpointStatuses.put(endpoint, false); + } + } + + public boolean isEndpointEnabled(String endpoint) { + if (endpoint.startsWith("/")) { + endpoint = endpoint.substring(1); + } + return endpointStatuses.getOrDefault(endpoint, true); + } + + public void addEndpointToGroup(String group, String endpoint) { + endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint); + } + + public void enableGroup(String group) { + Set endpoints = endpointGroups.get(group); + if (endpoints != null) { + for (String endpoint : endpoints) { + enableEndpoint(endpoint); + } + } + } + + public void disableGroup(String group) { + Set endpoints = endpointGroups.get(group); + if (endpoints != null) { + for (String endpoint : endpoints) { + disableEndpoint(endpoint); + } + } + } + + public void init() { + // Adding endpoints to "PageOps" group + addEndpointToGroup("PageOps", "remove-pages"); + addEndpointToGroup("PageOps", "merge-pdfs"); + addEndpointToGroup("PageOps", "split-pdfs"); + addEndpointToGroup("PageOps", "pdf-organizer"); + addEndpointToGroup("PageOps", "rotate-pdf"); + addEndpointToGroup("PageOps", "multi-page-layout"); + addEndpointToGroup("PageOps", "scale-pages"); + addEndpointToGroup("PageOps", "adjust-contrast"); + addEndpointToGroup("PageOps", "crop"); + addEndpointToGroup("PageOps", "auto-split-pdf"); + + // Adding endpoints to "Convert" group + addEndpointToGroup("Convert", "pdf-to-img"); + addEndpointToGroup("Convert", "img-to-pdf"); + addEndpointToGroup("Convert", "pdf-to-pdfa"); + addEndpointToGroup("Convert", "file-to-pdf"); + addEndpointToGroup("Convert", "xlsx-to-pdf"); + addEndpointToGroup("Convert", "pdf-to-word"); + addEndpointToGroup("Convert", "pdf-to-presentation"); + addEndpointToGroup("Convert", "pdf-to-text"); + addEndpointToGroup("Convert", "pdf-to-html"); + addEndpointToGroup("Convert", "pdf-to-xml"); + + // Adding endpoints to "Security" group + addEndpointToGroup("Security", "add-password"); + addEndpointToGroup("Security", "remove-password"); + addEndpointToGroup("Security", "change-permissions"); + addEndpointToGroup("Security", "add-watermark"); + addEndpointToGroup("Security", "cert-sign"); + addEndpointToGroup("Security", "sanitize-pdf"); + + + // Adding endpoints to "Other" group + addEndpointToGroup("Other", "ocr-pdf"); + addEndpointToGroup("Other", "add-image"); + addEndpointToGroup("Other", "compress-pdf"); + addEndpointToGroup("Other", "extract-images"); + addEndpointToGroup("Other", "change-metadata"); + addEndpointToGroup("Other", "extract-image-scans"); + addEndpointToGroup("Other", "sign"); + addEndpointToGroup("Other", "flatten"); + addEndpointToGroup("Other", "repair"); + addEndpointToGroup("Other", "remove-blanks"); + addEndpointToGroup("Other", "compare"); + addEndpointToGroup("Other", "add-page-numbers"); + addEndpointToGroup("Other", "auto-rename"); + + + + + //CLI + addEndpointToGroup("CLI", "compress-pdf"); + addEndpointToGroup("CLI", "extract-image-scans"); + addEndpointToGroup("CLI", "remove-blanks"); + addEndpointToGroup("CLI", "repair"); + addEndpointToGroup("CLI", "pdf-to-pdfa"); + addEndpointToGroup("CLI", "file-to-pdf"); + addEndpointToGroup("CLI", "xlsx-to-pdf"); + addEndpointToGroup("CLI", "pdf-to-word"); + addEndpointToGroup("CLI", "pdf-to-presentation"); + addEndpointToGroup("CLI", "pdf-to-text"); + addEndpointToGroup("CLI", "pdf-to-html"); + addEndpointToGroup("CLI", "pdf-to-xml"); + addEndpointToGroup("CLI", "ocr-pdf"); + + //python + addEndpointToGroup("Python", "extract-image-scans"); + addEndpointToGroup("Python", "remove-blanks"); + + + + //openCV + addEndpointToGroup("OpenCV", "extract-image-scans"); + addEndpointToGroup("OpenCV", "remove-blanks"); + + //LibreOffice + addEndpointToGroup("LibreOffice", "repair"); + addEndpointToGroup("LibreOffice", "file-to-pdf"); + addEndpointToGroup("LibreOffice", "xlsx-to-pdf"); + addEndpointToGroup("LibreOffice", "pdf-to-word"); + addEndpointToGroup("LibreOffice", "pdf-to-presentation"); + addEndpointToGroup("LibreOffice", "pdf-to-text"); + addEndpointToGroup("LibreOffice", "pdf-to-html"); + addEndpointToGroup("LibreOffice", "pdf-to-xml"); + + + //OCRmyPDF + addEndpointToGroup("OCRmyPDF", "compress-pdf"); + addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa"); + addEndpointToGroup("OCRmyPDF", "ocr-pdf"); + + //Java + addEndpointToGroup("Java", "merge-pdfs"); + addEndpointToGroup("Java", "remove-pages"); + addEndpointToGroup("Java", "split-pdfs"); + addEndpointToGroup("Java", "pdf-organizer"); + addEndpointToGroup("Java", "rotate-pdf"); + addEndpointToGroup("Java", "pdf-to-img"); + addEndpointToGroup("Java", "img-to-pdf"); + addEndpointToGroup("Java", "add-password"); + addEndpointToGroup("Java", "remove-password"); + addEndpointToGroup("Java", "change-permissions"); + addEndpointToGroup("Java", "add-watermark"); + addEndpointToGroup("Java", "add-image"); + addEndpointToGroup("Java", "extract-images"); + addEndpointToGroup("Java", "change-metadata"); + addEndpointToGroup("Java", "cert-sign"); + addEndpointToGroup("Java", "multi-page-layout"); + addEndpointToGroup("Java", "scale-pages"); + addEndpointToGroup("Java", "add-page-numbers"); + addEndpointToGroup("Java", "auto-rename"); + addEndpointToGroup("Java", "auto-split-pdf"); + addEndpointToGroup("Java", "sanitize-pdf"); + addEndpointToGroup("Java", "crop"); + + //Javascript + addEndpointToGroup("Javascript", "pdf-organizer"); + addEndpointToGroup("Javascript", "sign"); + addEndpointToGroup("Javascript", "compare"); + addEndpointToGroup("Javascript", "adjust-contrast"); + + + } + + private void processEnvironmentConfigs() { + String endpointsToRemove = System.getenv("ENDPOINTS_TO_REMOVE"); + String groupsToRemove = System.getenv("GROUPS_TO_REMOVE"); + + if (endpointsToRemove != null) { + String[] endpoints = endpointsToRemove.split(","); + for (String endpoint : endpoints) { + disableEndpoint(endpoint.trim()); + } + } + + if (groupsToRemove != null) { + String[] groups = groupsToRemove.split(","); + for (String group : groups) { + disableGroup(group.trim()); + } + } + } + +} + diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java b/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java index 3a4b5b1c..c1454a24 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 @@ -1,176 +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.newContentStreamAfter(), page.getResources(), pdfDoc); - - String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(pdfDoc.getNumberOfPages())) : String.valueOf(pageNumber); - - PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA); - float textWidth = font.getWidth(text, fontSize); - float textHeight = font.getAscent(text, fontSize) - font.getDescent(text, fontSize); - - float x, y; - TextAlignment alignment; - - int xGroup = (position - 1) % 3; - int yGroup = 2 - (position - 1) / 3; - - switch (xGroup) { - case 0: // left - x = pageSize.getLeft() + marginFactor * pageSize.getWidth(); - alignment = TextAlignment.LEFT; - break; - case 1: // center - x = pageSize.getLeft() + (pageSize.getWidth()) / 2; - alignment = TextAlignment.CENTER; - break; - default: // right - x = pageSize.getRight() - marginFactor * pageSize.getWidth(); - alignment = TextAlignment.RIGHT; - break; - } - - switch (yGroup) { - case 0: // bottom - y = pageSize.getBottom() + marginFactor * pageSize.getHeight(); - break; - case 1: // middle - y = pageSize.getBottom() + (pageSize.getHeight() ) / 2; - break; - default: // top - y = pageSize.getTop() - marginFactor * pageSize.getHeight(); - break; - } - - new Canvas(pdfCanvas, page.getPageSize()) - .showTextAligned(new Paragraph(text).setFont(font).setFontSize(fontSize), x, y, alignment); - - pageNumber++; - } - - - pdfDoc.close(); - byte[] resultBytes = baos.toByteArray(); - - return ResponseEntity.ok() - .header("Content-Type", "application/pdf; charset=UTF-8") - .header("Content-Disposition", "inline; filename=" + URLEncoder.encode(file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", "UTF-8")) - .body(resultBytes); - } - - - -} +package stirling.software.SPDF.controller.api.other; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import stirling.software.SPDF.utils.GeneralUtils; +import stirling.software.SPDF.utils.PdfUtils; +import stirling.software.SPDF.utils.WebResponseUtils; +import org.apache.pdfbox.pdmodel.*; +import org.apache.pdfbox.pdmodel.common.*; +import org.apache.pdfbox.pdmodel.PDPageContentStream.*; +import org.springframework.web.bind.annotation.*; +import org.springframework.http.*; +import org.springframework.web.multipart.MultipartFile; +import io.swagger.v3.oas.annotations.*; +import io.swagger.v3.oas.annotations.media.*; +import io.swagger.v3.oas.annotations.parameters.*; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.tomcat.util.http.ResponseUtil; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URLEncoder; +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; + +import com.itextpdf.io.font.constants.StandardFonts; +import com.itextpdf.kernel.font.PdfFont; +import com.itextpdf.kernel.font.PdfFontFactory; +import com.itextpdf.kernel.geom.Rectangle; +import com.itextpdf.kernel.pdf.PdfReader; +import com.itextpdf.kernel.pdf.PdfWriter; +import com.itextpdf.kernel.pdf.PdfDocument; +import com.itextpdf.kernel.pdf.PdfPage; +import com.itextpdf.kernel.pdf.canvas.PdfCanvas; +import com.itextpdf.layout.Canvas; +import com.itextpdf.layout.element.Paragraph; +import com.itextpdf.layout.properties.TextAlignment; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.io.*; + +@RestController +@Tag(name = "Other", description = "Other APIs") +public class PageNumbersController { + + private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class); + + @PostMapping(value = "/add-page-numbers", consumes = "multipart/form-data") + @Operation(summary = "Add page numbers to a PDF document", description = "This operation takes an input PDF file and adds page numbers to it. Input:PDF Output:PDF Type:SISO") + public ResponseEntity addPageNumbers( + @Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file, + @Parameter(description = "Custom margin: small/medium/large", required = true, schema = @Schema(type = "string", allowableValues = {"small", "medium", "large"})) @RequestParam("customMargin") String customMargin, + @Parameter(description = "Position: 1 of 9 positions", required = true, schema = @Schema(type = "integer", minimum = "1", maximum = "9")) @RequestParam("position") int position, + @Parameter(description = "Starting number", required = true, schema = @Schema(type = "integer", minimum = "1")) @RequestParam("startingNumber") int startingNumber, + @Parameter(description = "Which pages to number, default all", required = false, schema = @Schema(type = "string")) @RequestParam(value = "pagesToNumber", required = false) String pagesToNumber, + @Parameter(description = "Custom text: defaults to just number but can have things like \"Page {n} of {p}\"", required = false, schema = @Schema(type = "string")) @RequestParam(value = "customText", required = false) String customText) + throws IOException { + + byte[] fileBytes = file.getBytes(); + ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes); + + int pageNumber = startingNumber; + float marginFactor; + switch (customMargin.toLowerCase()) { + case "small": + marginFactor = 0.02f; + break; + case "medium": + marginFactor = 0.035f; + break; + case "large": + marginFactor = 0.05f; + break; + case "x-large": + marginFactor = 0.1f; + break; + default: + marginFactor = 0.035f; + break; + } + + float fontSize = 12.0f; + + PdfReader reader = new PdfReader(bais); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PdfWriter writer = new PdfWriter(baos); + + PdfDocument pdfDoc = new PdfDocument(reader, writer); + + List pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), pdfDoc.getNumberOfPages()); + + for (int i : pagesToNumberList) { + PdfPage page = pdfDoc.getPage(i+1); + Rectangle pageSize = page.getPageSize(); + PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamAfter(), page.getResources(), pdfDoc); + + String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(pdfDoc.getNumberOfPages())) : String.valueOf(pageNumber); + + PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA); + float textWidth = font.getWidth(text, fontSize); + float textHeight = font.getAscent(text, fontSize) - font.getDescent(text, fontSize); + + float x, y; + TextAlignment alignment; + + int xGroup = (position - 1) % 3; + int yGroup = 2 - (position - 1) / 3; + + switch (xGroup) { + case 0: // left + x = pageSize.getLeft() + marginFactor * pageSize.getWidth(); + alignment = TextAlignment.LEFT; + break; + case 1: // center + x = pageSize.getLeft() + (pageSize.getWidth()) / 2; + alignment = TextAlignment.CENTER; + break; + default: // right + x = pageSize.getRight() - marginFactor * pageSize.getWidth(); + alignment = TextAlignment.RIGHT; + break; + } + + switch (yGroup) { + case 0: // bottom + y = pageSize.getBottom() + marginFactor * pageSize.getHeight(); + break; + case 1: // middle + y = pageSize.getBottom() + (pageSize.getHeight() ) / 2; + break; + default: // top + y = pageSize.getTop() - marginFactor * pageSize.getHeight(); + break; + } + + new Canvas(pdfCanvas, page.getPageSize()) + .showTextAligned(new Paragraph(text).setFont(font).setFontSize(fontSize), x, y, alignment); + + pageNumber++; + } + + + pdfDoc.close(); + byte[] resultBytes = baos.toByteArray(); + + return ResponseEntity.ok() + .header("Content-Type", "application/pdf; charset=UTF-8") + .header("Content-Disposition", "inline; filename=" + URLEncoder.encode(file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", "UTF-8")) + .body(resultBytes); + } + + + +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java b/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java new file mode 100644 index 00000000..ccc19b09 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java @@ -0,0 +1,140 @@ +package stirling.software.SPDF.controller.api.security; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.PDPageTree; +import org.apache.pdfbox.pdmodel.common.PDMetadata; +import org.apache.pdfbox.pdmodel.common.PDStream; +import org.apache.pdfbox.pdmodel.interactive.action.*; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; +import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; +import org.apache.pdfbox.pdmodel.interactive.form.PDField; +import org.apache.pdfbox.pdmodel.interactive.form.PDNonTerminalField; +import org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import stirling.software.SPDF.utils.WebResponseUtils; + +import java.io.IOException; +import java.io.InputStream; + +@RestController +public class SanitizeController { + + @PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf") + @Operation(summary = "Sanitize a PDF file", + description = "This endpoint processes a PDF file and removes specific elements based on the provided options. Input:PDF Output:PDF Type:SISO") + public ResponseEntity sanitizePDF( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input PDF file to be sanitized") + MultipartFile inputFile, + @RequestParam(name = "removeJavaScript", required = false, defaultValue = "true") + @Parameter(description = "Remove JavaScript actions from the PDF if set to true") + Boolean removeJavaScript, + @RequestParam(name = "removeEmbeddedFiles", required = false, defaultValue = "true") + @Parameter(description = "Remove embedded files from the PDF if set to true") + Boolean removeEmbeddedFiles, + @RequestParam(name = "removeMetadata", required = false, defaultValue = "true") + @Parameter(description = "Remove metadata from the PDF if set to true") + Boolean removeMetadata, + @RequestParam(name = "removeLinks", required = false, defaultValue = "true") + @Parameter(description = "Remove links from the PDF if set to true") + Boolean removeLinks, + @RequestParam(name = "removeFonts", required = false, defaultValue = "true") + @Parameter(description = "Remove fonts from the PDF if set to true") + Boolean removeFonts) throws IOException { + + try (PDDocument document = PDDocument.load(inputFile.getInputStream())) { + if (removeJavaScript) { + sanitizeJavaScript(document); + } + + if (removeEmbeddedFiles) { + sanitizeEmbeddedFiles(document); + } + + if (removeMetadata) { + sanitizeMetadata(document); + } + + if (removeLinks) { + sanitizeLinks(document); + } + + if (removeFonts) { + sanitizeFonts(document); + } + + return WebResponseUtils.pdfDocToWebResponse(document, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_sanitized.pdf"); + } + } + private void sanitizeJavaScript(PDDocument document) throws IOException { + for (PDPage page : document.getPages()) { + for (PDAnnotation annotation : page.getAnnotations()) { + if (annotation instanceof PDAnnotationWidget) { + PDAnnotationWidget widget = (PDAnnotationWidget) annotation; + PDAction action = widget.getAction(); + if (action instanceof PDActionJavaScript) { + widget.setAction(null); + } + } + } + PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); + if (acroForm != null) { + for (PDField field : acroForm.getFields()) { + if (field.getActions().getF() instanceof PDActionJavaScript) { + field.getActions().setF(null); + } + } + } + } + } + + private void sanitizeEmbeddedFiles(PDDocument document) { + PDPageTree allPages = document.getPages(); + + for (PDPage page : allPages) { + PDResources res = page.getResources(); + + // Remove embedded files from the PDF + res.getCOSObject().removeItem(COSName.getPDFName("EmbeddedFiles")); + } + } + + + private void sanitizeMetadata(PDDocument document) { + PDMetadata metadata = document.getDocumentCatalog().getMetadata(); + if (metadata != null) { + document.getDocumentCatalog().setMetadata(null); + } + } + + + + private void sanitizeLinks(PDDocument document) throws IOException { + for (PDPage page : document.getPages()) { + for (PDAnnotation annotation : page.getAnnotations()) { + if (annotation instanceof PDAnnotationLink) { + PDAction action = ((PDAnnotationLink) annotation).getAction(); + if (action instanceof PDActionLaunch || action instanceof PDActionURI) { + ((PDAnnotationLink) annotation).setAction(null); + } + } + } + } + } + + private void sanitizeFonts(PDDocument document) { + for (PDPage page : document.getPages()) { + page.getResources().getCOSObject().removeItem(COSName.getPDFName("Font")); + } + } + +} diff --git a/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java b/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java index 0dbb226b..54f88618 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java @@ -1,46 +1,53 @@ -package stirling.software.SPDF.controller.web; - -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; - -import io.swagger.v3.oas.annotations.Hidden; -import io.swagger.v3.oas.annotations.tags.Tag; - -@Controller -@Tag(name = "Security", description = "Security APIs") -public class SecurityWebController { - @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) { - model.addAttribute("currentPage", "change-permissions"); - return "security/change-permissions"; - } - - @GetMapping("/remove-password") - @Hidden - public String removePasswordForm(Model model) { - model.addAttribute("currentPage", "remove-password"); - return "security/remove-password"; - } - - @GetMapping("/add-watermark") - @Hidden - public String addWatermarkForm(Model model) { - 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"; - } -} +package stirling.software.SPDF.controller.web; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Controller +@Tag(name = "Security", description = "Security APIs") +public class SecurityWebController { + @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) { + model.addAttribute("currentPage", "change-permissions"); + return "security/change-permissions"; + } + + @GetMapping("/remove-password") + @Hidden + public String removePasswordForm(Model model) { + model.addAttribute("currentPage", "remove-password"); + return "security/remove-password"; + } + + @GetMapping("/add-watermark") + @Hidden + public String addWatermarkForm(Model model) { + 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", ""); + return "security/sanitize-pdf"; + } +} diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 0da26ea7..c16aa404 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -152,6 +152,10 @@ home.crop.desc=Crop a PDF to reduce its size (maintains text!) home.autoSplitPDF.title=Auto Split Pages home.autoSplitPDF.desc=Auto Split Scanned PDF with physical scanned page splitter QR Code +home.sanitizePdf.title=Sanitize +home.sanitizePdf.desc=Remove scripts and other elements from PDF files + + error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect downloadPdf=Download PDF @@ -159,6 +163,49 @@ text=Text font=Font selectFillter=-- Select -- pageNum=Page Number +sizes.small=Small +sizes.medium=Medium +sizes.large=Large +sizes.x-large=X-Large + +sanitizePDF.title=Sanitize PDF +sanitizePDF.header=Sanitize a PDF file +sanitizePDF.selectText.1=Remove JavaScript actions +sanitizePDF.selectText.2=Remove embedded files +sanitizePDF.selectText.3=Remove metadata +sanitizePDF.selectText.4=Remove links +sanitizePDF.selectText.5=Remove fonts +sanitizePDF.submit=Sanitize PDF + +addPageNumbers.title=Add Page Numbers +addPageNumbers.header=Add Page Numbers +addPageNumbers.selectText.1=Select PDF file: +addPageNumbers.selectText.2=Margin Size +addPageNumbers.selectText.3=Position +addPageNumbers.selectText.4=Starting Number +addPageNumbers.selectText.5=Pages to Number +addPageNumbers.selectText.6=Custom Text +addPageNumbers.submit=Add Page Numbers + +auto-rename.title=Auto Rename +auto-rename.header=Auto Rename PDF +auto-rename.submit=Auto Rename + +adjustContrast.title=Adjust Contrast +adjustContrast.header=Adjust Contrast +adjustContrast.contrast=Contrast: +adjustContrast.brightness=Brightness: +adjustContrast.saturation=Saturation: +adjustContrast.download=Download + +crop.title=Crop +crop.header=Crop Image +crop.submit=Submit + +autoSplitPDF.title=Auto Split PDF +autoSplitPDF.header=Auto Split PDF +autoSplitPDF.submit=Submit + pipeline.title=Pipeline diff --git a/src/main/resources/static/images/sanitize.svg b/src/main/resources/static/images/sanitize.svg new file mode 100644 index 00000000..fc4dd2f9 --- /dev/null +++ b/src/main/resources/static/images/sanitize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/resources/templates/crop.html b/src/main/resources/templates/crop.html index b3abeba3..a0e958ea 100644 --- a/src/main/resources/templates/crop.html +++ b/src/main/resources/templates/crop.html @@ -1,105 +1,103 @@ - - - - - - - -
-
-
-

-
-
-
-

-
- - - - - - -
- - - -
-
-
-
-
-
- - \ 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 18425f11..84a1e371 100644 --- a/src/main/resources/templates/fragments/navbar.html +++ b/src/main/resources/templates/fragments/navbar.html @@ -40,7 +40,7 @@ --> -