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