Merge pull request from Frooodle/pipeline

Pipeline changes, fonts, and watermark fixes
This commit is contained in:
Anthony Stirling 2023-07-02 18:21:17 +01:00 committed by GitHub
commit 182231a183
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 1729 additions and 173 deletions
DockerfileREADME.md
src/main
java/stirling/software/SPDF
resources

@ -5,6 +5,12 @@ FROM frooodle/stirling-pdf-base:latest
RUN mkdir /scripts RUN mkdir /scripts
COPY ./scripts/* /scripts/ COPY ./scripts/* /scripts/
#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 the application JAR file
COPY build/libs/*.jar app.jar COPY build/libs/*.jar app.jar

@ -119,7 +119,7 @@ services:
Please view https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md Please view https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md
## Want to add your own language? ## Want to add your own language?
Stirling PDF currently supports Stirling PDF currently supports 16!
- English (English) (en_GB) - English (English) (en_GB)
- Arabic (العربية) (ar_AR) - Arabic (العربية) (ar_AR)
- German (Deutsch) (de_DE) - German (Deutsch) (de_DE)
@ -132,6 +132,10 @@ Stirling PDF currently supports
- Polish (Polski) (pl_PL) - Polish (Polski) (pl_PL)
- Romanian (Română) (ro_RO) - Romanian (Română) (ro_RO)
- Korean (한국어) (ko_KR) - Korean (한국어) (ko_KR)
- Portuguese Brazilian (Português) (pt_BR)
- Russian (Русский) (ru_RU)
- Basque (Euskara) (eu_ES)
- Japanese (日本語) (ja_JP)
If you want to add your own language to Stirling-PDF please refer If you want to add your own language to Stirling-PDF please refer
https://github.com/Frooodle/Stirling-PDF/blob/main/HowToAddNewLanguage.md https://github.com/Frooodle/Stirling-PDF/blob/main/HowToAddNewLanguage.md

@ -1,16 +1,15 @@
package stirling.software.SPDF; package stirling.software.SPDF;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.awt.*; import org.springframework.core.env.Environment;
import java.net.URI; import org.springframework.scheduling.annotation.EnableScheduling;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
@SpringBootApplication @SpringBootApplication
@EnableScheduling
public class SPdfApplication { public class SPdfApplication {
@Autowired @Autowired

@ -1,4 +1,6 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.util.List;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -7,27 +9,57 @@ import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Arrays;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class CleanUrlInterceptor implements HandlerInterceptor { public class CleanUrlInterceptor implements HandlerInterceptor {
private static final Pattern LANG_PATTERN = Pattern.compile("&?lang=([^&]+)"); private static final List<String> ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints");
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String queryString = request.getQueryString(); String queryString = request.getQueryString();
if (queryString != null && !queryString.isEmpty()) { if (queryString != null && !queryString.isEmpty()) {
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
// Keep the lang parameter if it exists Map<String, String> parameters = new HashMap<>();
Matcher langMatcher = LANG_PATTERN.matcher(queryString);
String langQueryString = langMatcher.find() ? "lang=" + langMatcher.group(1) : "";
// Check if there are any other query parameters besides the lang parameter // Keep only the allowed parameters
String remainingQueryString = queryString.replaceAll(LANG_PATTERN.pattern(), "").replaceAll("&+", "&").replaceAll("^&|&$", ""); String[] queryParameters = queryString.split("&");
for (String param : queryParameters) {
String[] keyValue = param.split("=");
if (keyValue.length != 2) {
continue;
}
if (ALLOWED_PARAMS.contains(keyValue[0])) {
parameters.put(keyValue[0], keyValue[1]);
}
}
if (!remainingQueryString.isEmpty()) { // If there are any parameters that are not allowed
// Redirect to the URL without other query parameters if (parameters.size() != queryParameters.length) {
String redirectUrl = requestURI + (langQueryString.isEmpty() ? "" : "?" + langQueryString); // Construct new query string
StringBuilder newQueryString = new StringBuilder();
for (Map.Entry<String, String> entry : parameters.entrySet()) {
if (newQueryString.length() > 0) {
newQueryString.append("&");
}
newQueryString.append(entry.getKey()).append("=").append(entry.getValue());
}
// Redirect to the URL with only allowed query parameters
String redirectUrl = requestURI + "?" + newQueryString;
response.sendRedirect(redirectUrl); response.sendRedirect(redirectUrl);
return false; return false;
} }
@ -36,10 +68,12 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
} }
@Override @Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) {
} }
@Override @Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
} }
} }

@ -28,6 +28,7 @@ public class MetricsFilter extends OncePerRequestFilter {
throws ServletException, IOException { throws ServletException, IOException {
String uri = request.getRequestURI(); String uri = request.getRequestURI();
//System.out.println("uri="+uri + ", method=" + request.getMethod() );
// Ignore static resources // Ignore static resources
if (!(uri.startsWith("/js") || uri.startsWith("/images") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".svg")|| uri.endsWith(".js") || uri.contains("swagger") || uri.startsWith("/api"))) { if (!(uri.startsWith("/js") || uri.startsWith("/images") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".svg")|| uri.endsWith(".js") || uri.contains("swagger") || uri.startsWith("/api"))) {
Counter counter = Counter.builder("http.requests") Counter counter = Counter.builder("http.requests")
@ -36,6 +37,7 @@ public class MetricsFilter extends OncePerRequestFilter {
.register(meterRegistry); .register(meterRegistry);
counter.increment(); counter.increment();
//System.out.println("Counted");
} }
filterChain.doFilter(request, response); filterChain.doFilter(request, response);

@ -17,9 +17,11 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "General", description = "General APIs")
public class MergeController { public class MergeController {
private static final Logger logger = LoggerFactory.getLogger(MergeController.class); private static final Logger logger = LoggerFactory.getLogger(MergeController.class);
@ -47,7 +49,7 @@ public class MergeController {
@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs") @PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
@Operation( @Operation(
summary = "Merge multiple PDF files into one", summary = "Merge multiple PDF files into one",
description = "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided." description = "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO"
) )
public ResponseEntity<byte[]> mergePdfs( public ResponseEntity<byte[]> mergePdfs(
@RequestPart(required = true, value = "fileInput") @RequestPart(required = true, value = "fileInput")

@ -24,15 +24,17 @@ import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "General", description = "General APIs")
public class MultiPageLayoutController { public class MultiPageLayoutController {
private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class); private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class);
@PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data") @PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data")
@Operation(summary = "Merge multiple pages of a PDF document into a single page", description = "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file.") @Operation(summary = "Merge multiple pages of a PDF document into a single page", description = "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> mergeMultiplePagesIntoOne( public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file, @Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
@Parameter(description = "The number of pages to fit onto a single sheet in the output PDF. Acceptable values are 2, 3, 4, 9, 16.", required = true, schema = @Schema(type = "integer", allowableValues = { @Parameter(description = "The number of pages to fit onto a single sheet in the output PDF. Acceptable values are 2, 3, 4, 9, 16.", required = true, schema = @Schema(type = "integer", allowableValues = {

@ -18,16 +18,18 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "General", description = "General APIs")
public class RearrangePagesPDFController { public class RearrangePagesPDFController {
private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class); private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class);
@PostMapping(consumes = "multipart/form-data", value = "/remove-pages") @PostMapping(consumes = "multipart/form-data", value = "/remove-pages")
@Operation(summary = "Remove pages from a PDF file", description = "This endpoint removes specified pages from a given PDF file. Users can provide a comma-separated list of page numbers or ranges to delete.") @Operation(summary = "Remove pages from a PDF file", description = "This endpoint removes specified pages from a given PDF file. Users can provide a comma-separated list of page numbers or ranges to delete. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> deletePages( public ResponseEntity<byte[]> deletePages(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file from which pages will be removed") MultipartFile pdfFile, @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file from which pages will be removed") MultipartFile pdfFile,
@RequestParam("pagesToDelete") @Parameter(description = "Comma-separated list of pages or page ranges to delete, e.g., '1,3,5-8'") String pagesToDelete) @RequestParam("pagesToDelete") @Parameter(description = "Comma-separated list of pages or page ranges to delete, e.g., '1,3,5-8'") String pagesToDelete)
@ -151,7 +153,7 @@ public class RearrangePagesPDFController {
} }
@PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages") @PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages")
@Operation(summary = "Rearrange pages in a PDF file", description = "This endpoint rearranges pages in a given PDF file based on the specified page order or custom mode. Users can provide a page order as a comma-separated list of page numbers or page ranges, or a custom mode.") @Operation(summary = "Rearrange pages in a PDF file", description = "This endpoint rearranges pages in a given PDF file based on the specified page order or custom mode. Users can provide a page order as a comma-separated list of page numbers or page ranges, or a custom mode. Input:PDF Output:PDF")
public ResponseEntity<byte[]> rearrangePages( public ResponseEntity<byte[]> rearrangePages(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to rearrange pages") MultipartFile pdfFile, @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to rearrange pages") MultipartFile pdfFile,
@RequestParam(required = false, value = "pageOrder") @Parameter(description = "The new page order as a comma-separated list of page numbers, page ranges (e.g., '1,3,5-7'), or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')") String pageOrder, @RequestParam(required = false, value = "pageOrder") @Parameter(description = "The new page order as a comma-separated list of page numbers, page ranges (e.g., '1,3,5-7'), or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')") String pageOrder,

@ -16,9 +16,11 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "General", description = "General APIs")
public class RotationController { public class RotationController {
private static final Logger logger = LoggerFactory.getLogger(RotationController.class); private static final Logger logger = LoggerFactory.getLogger(RotationController.class);
@ -26,7 +28,7 @@ public class RotationController {
@PostMapping(consumes = "multipart/form-data", value = "/rotate-pdf") @PostMapping(consumes = "multipart/form-data", value = "/rotate-pdf")
@Operation( @Operation(
summary = "Rotate a PDF file", summary = "Rotate a PDF file",
description = "This endpoint rotates a given PDF file by a specified angle. The angle must be a multiple of 90." description = "This endpoint rotates a given PDF file by a specified angle. The angle must be a multiple of 90. Input:PDF Output:PDF Type:SISO"
) )
public ResponseEntity<byte[]> rotatePDF( public ResponseEntity<byte[]> rotatePDF(
@RequestPart(required = true, value = "fileInput") @RequestPart(required = true, value = "fileInput")

@ -39,15 +39,17 @@ import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "General", description = "General APIs")
public class ScalePagesController { public class ScalePagesController {
private static final Logger logger = LoggerFactory.getLogger(ScalePagesController.class); private static final Logger logger = LoggerFactory.getLogger(ScalePagesController.class);
@PostMapping(value = "/scale-pages", consumes = "multipart/form-data") @PostMapping(value = "/scale-pages", consumes = "multipart/form-data")
@Operation(summary = "Change the size of a PDF page/document", description = "This operation takes an input PDF file and the size to scale the pages to in the output PDF file.") @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<byte[]> scalePages( public ResponseEntity<byte[]> scalePages(
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file, @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 = {

@ -15,9 +15,6 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@ -28,17 +25,19 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; 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.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "General", description = "General APIs")
public class SplitPDFController { public class SplitPDFController {
private static final Logger logger = LoggerFactory.getLogger(SplitPDFController.class); private static final Logger logger = LoggerFactory.getLogger(SplitPDFController.class);
@PostMapping(consumes = "multipart/form-data", value = "/split-pages") @PostMapping(consumes = "multipart/form-data", value = "/split-pages")
@Operation(summary = "Split a PDF file into separate documents", @Operation(summary = "Split a PDF file into separate documents",
description = "This endpoint splits a given PDF file into separate documents based on the specified page numbers or ranges. Users can specify pages using individual numbers, ranges, or 'all' for every page.") description = "This endpoint splits a given PDF file into separate documents based on the specified page numbers or ranges. Users can specify pages using individual numbers, ranges, or 'all' for every page. Input:PDF Output:PDF Type:SIMO")
public ResponseEntity<byte[]> splitPdf( public ResponseEntity<byte[]> splitPdf(
@RequestPart(required = true, value = "fileInput") @RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be split") @Parameter(description = "The input PDF file to be split")

@ -20,16 +20,18 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertImgPDFController { public class ConvertImgPDFController {
private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class); private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class);
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-img") @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-img")
@Operation(summary = "Convert PDF to image(s)", @Operation(summary = "Convert PDF to image(s)",
description = "This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images.") description = "This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images. Input:PDF Output:Image Type:SI-Conditional")
public ResponseEntity<Resource> convertToImage( public ResponseEntity<Resource> convertToImage(
@RequestPart(required = true, value = "fileInput") @RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be converted") @Parameter(description = "The input PDF file to be converted")
@ -83,7 +85,7 @@ public class ConvertImgPDFController {
@PostMapping(consumes = "multipart/form-data", value = "/img-to-pdf") @PostMapping(consumes = "multipart/form-data", value = "/img-to-pdf")
@Operation(summary = "Convert images to a PDF file", @Operation(summary = "Convert images to a PDF file",
description = "This endpoint converts one or more images to a PDF file. Users can specify whether to stretch the images to fit the PDF page, and whether to automatically rotate the images.") description = "This endpoint converts one or more images to a PDF file. Users can specify whether to stretch the images to fit the PDF page, and whether to automatically rotate the images. Input:Image Output:PDF Type:SISO?")
public ResponseEntity<byte[]> convertToPdf( public ResponseEntity<byte[]> convertToPdf(
@RequestPart(required = true, value = "fileInput") @RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input images to be converted to a PDF file") @Parameter(description = "The input images to be converted to a PDF file")

@ -17,10 +17,12 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertOfficeController { public class ConvertOfficeController {
public byte[] convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException { public byte[] convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
@ -57,8 +59,8 @@ public class ConvertOfficeController {
@PostMapping(consumes = "multipart/form-data", value = "/file-to-pdf") @PostMapping(consumes = "multipart/form-data", value = "/file-to-pdf")
@Operation( @Operation(
summary = "Convert a file to a PDF using OCR", summary = "Convert a file to a PDF using LibreOffice",
description = "This endpoint converts a given file to a PDF using Optical Character Recognition (OCR). The filename of the resulting PDF will be the original filename with '_convertedToPDF.pdf' appended." description = "This endpoint converts a given file to a PDF using LibreOffice API Input:Any Output:PDF Type:SISO"
) )
public ResponseEntity<byte[]> processPdfWithOCR( public ResponseEntity<byte[]> processPdfWithOCR(
@RequestPart(required = true, value = "fileInput") @RequestPart(required = true, value = "fileInput")

@ -12,13 +12,15 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.PDFToFile; import stirling.software.SPDF.utils.PDFToFile;
@RestController @RestController
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertPDFToOffice { public class ConvertPDFToOffice {
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-html") @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-html")
@Operation(summary = "Convert PDF to HTML", description = "This endpoint converts a PDF file to HTML format.") @Operation(summary = "Convert PDF to HTML", description = "This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO")
public ResponseEntity<byte[]> processPdfToHTML( public ResponseEntity<byte[]> processPdfToHTML(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to HTML format", required = true) MultipartFile inputFile) @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to HTML format", required = true) MultipartFile inputFile)
throws IOException, InterruptedException { throws IOException, InterruptedException {
@ -27,7 +29,7 @@ public class ConvertPDFToOffice {
} }
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-presentation") @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-presentation")
@Operation(summary = "Convert PDF to Presentation format", description = "This endpoint converts a given PDF file to a Presentation format.") @Operation(summary = "Convert PDF to Presentation format", description = "This endpoint converts a given PDF file to a Presentation format. Input:PDF Output:PPT Type:SISO")
public ResponseEntity<byte[]> processPdfToPresentation( public ResponseEntity<byte[]> processPdfToPresentation(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile, @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile,
@RequestParam("outputFormat") @Parameter(description = "The output Presentation format", schema = @Schema(allowableValues = { @RequestParam("outputFormat") @Parameter(description = "The output Presentation format", schema = @Schema(allowableValues = {
@ -38,7 +40,7 @@ public class ConvertPDFToOffice {
} }
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-text") @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-text")
@Operation(summary = "Convert PDF to Text or RTF format", description = "This endpoint converts a given PDF file to Text or RTF format.") @Operation(summary = "Convert PDF to Text or RTF format", description = "This endpoint converts a given PDF file to Text or RTF format. Input:PDF Output:TXT Type:SISO")
public ResponseEntity<byte[]> processPdfToRTForTXT( public ResponseEntity<byte[]> processPdfToRTForTXT(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile, @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile,
@RequestParam("outputFormat") @Parameter(description = "The output Text or RTF format", schema = @Schema(allowableValues = { @RequestParam("outputFormat") @Parameter(description = "The output Text or RTF format", schema = @Schema(allowableValues = {
@ -49,7 +51,7 @@ public class ConvertPDFToOffice {
} }
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-word") @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-word")
@Operation(summary = "Convert PDF to Word document", description = "This endpoint converts a given PDF file to a Word document format.") @Operation(summary = "Convert PDF to Word document", description = "This endpoint converts a given PDF file to a Word document format. Input:PDF Output:WORD Type:SISO")
public ResponseEntity<byte[]> processPdfToWord( public ResponseEntity<byte[]> processPdfToWord(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile, @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile,
@RequestParam("outputFormat") @Parameter(description = "The output Word document format", schema = @Schema(allowableValues = { @RequestParam("outputFormat") @Parameter(description = "The output Word document format", schema = @Schema(allowableValues = {
@ -60,7 +62,7 @@ public class ConvertPDFToOffice {
} }
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-xml") @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-xml")
@Operation(summary = "Convert PDF to XML", description = "This endpoint converts a PDF file to an XML file.") @Operation(summary = "Convert PDF to XML", description = "This endpoint converts a PDF file to an XML file. Input:PDF Output:XML Type:SISO")
public ResponseEntity<byte[]> processPdfToXML( public ResponseEntity<byte[]> processPdfToXML(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to an XML file", required = true) MultipartFile inputFile) @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to an XML file", required = true) MultipartFile inputFile)
throws IOException, InterruptedException { throws IOException, InterruptedException {

@ -14,16 +14,18 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertPDFToPDFA { public class ConvertPDFToPDFA {
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-pdfa") @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-pdfa")
@Operation( @Operation(
summary = "Convert a PDF to a PDF/A", summary = "Convert a PDF to a PDF/A",
description = "This endpoint converts a PDF file to a PDF/A file. PDF/A is a format designed for long-term archiving of digital documents." description = "This endpoint converts a PDF file to a PDF/A file. PDF/A is a format designed for long-term archiving of digital documents. Input:PDF Output:PDF Type:SISO"
) )
public ResponseEntity<byte[]> pdfToPdfA( public ResponseEntity<byte[]> pdfToPdfA(
@RequestPart(required = true, value = "fileInput") @RequestPart(required = true, value = "fileInput")

@ -0,0 +1,162 @@
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);
}
}
}

@ -28,17 +28,19 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import stirling.software.SPDF.pdf.ImageFinder; 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.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "Other", description = "Other APIs")
public class BlankPageController { public class BlankPageController {
@PostMapping(consumes = "multipart/form-data", value = "/remove-blanks") @PostMapping(consumes = "multipart/form-data", value = "/remove-blanks")
@Operation( @Operation(
summary = "Remove blank pages from a PDF file", summary = "Remove blank pages from a PDF file",
description = "This endpoint removes blank pages from a given PDF file. Users can specify the threshold and white percentage to tune the detection of blank pages." description = "This endpoint removes blank pages from a given PDF file. Users can specify the threshold and white percentage to tune the detection of blank pages. Input:PDF Output:PDF Type:SISO"
) )
public ResponseEntity<byte[]> removeBlankPages( public ResponseEntity<byte[]> removeBlankPages(
@RequestPart(required = true, value = "fileInput") @RequestPart(required = true, value = "fileInput")
@ -71,7 +73,7 @@ public class BlankPageController {
pagesToKeepIndex.add(pageIndex); pagesToKeepIndex.add(pageIndex);
System.out.println("page " + pageIndex + " has text"); System.out.println("page " + pageIndex + " has text");
} else { } else {
boolean hasImages = hasImagesOnPage(page); boolean hasImages = PdfUtils.hasImagesOnPage(page);
if (hasImages) { if (hasImages) {
System.out.println("page " + pageIndex + " has image"); System.out.println("page " + pageIndex + " has image");
@ -120,9 +122,5 @@ public class BlankPageController {
} }
private static boolean hasImagesOnPage(PDPage page) throws IOException {
ImageFinder imageFinder = new ImageFinder(page);
imageFinder.processPage(page);
return imageFinder.hasImages();
}
} }

@ -31,17 +31,19 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "Other", description = "Other APIs")
public class CompressController { public class CompressController {
private static final Logger logger = LoggerFactory.getLogger(CompressController.class); private static final Logger logger = LoggerFactory.getLogger(CompressController.class);
@PostMapping(consumes = "multipart/form-data", value = "/compress-pdf") @PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
@Operation(summary = "Optimize PDF file", description = "This endpoint accepts a PDF file and optimizes it based on the provided parameters.") @Operation(summary = "Optimize PDF file", description = "This endpoint accepts a PDF file and optimizes it based on the provided parameters. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> optimizePdf( public ResponseEntity<byte[]> optimizePdf(
@RequestPart(value = "fileInput") @Parameter(description = "The input PDF file to be optimized.", required = true) MultipartFile inputFile, @RequestPart(value = "fileInput") @Parameter(description = "The input PDF file to be optimized.", required = true) MultipartFile inputFile,
@RequestParam(required = false, value = "optimizeLevel") @Parameter(description = "The level of optimization to apply to the PDF file. Higher values indicate greater compression but may reduce quality.", schema = @Schema(allowableValues = { @RequestParam(required = false, value = "optimizeLevel") @Parameter(description = "The level of optimization to apply to the PDF file. Higher values indicate greater compression but may reduce quality.", schema = @Schema(allowableValues = {

@ -31,17 +31,19 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "Other", description = "Other APIs")
public class ExtractImageScansController { public class ExtractImageScansController {
private static final Logger logger = LoggerFactory.getLogger(ExtractImageScansController.class); private static final Logger logger = LoggerFactory.getLogger(ExtractImageScansController.class);
@PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans") @PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans")
@Operation(summary = "Extract image scans from an input file", @Operation(summary = "Extract image scans from an input file",
description = "This endpoint extracts image scans from a given file based on certain parameters. Users can specify angle threshold, tolerance, minimum area, minimum contour area, and border size.") description = "This endpoint extracts image scans from a given file based on certain parameters. Users can specify angle threshold, tolerance, minimum area, minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP Type:SIMO")
public ResponseEntity<byte[]> extractImageScans( public ResponseEntity<byte[]> extractImageScans(
@RequestPart(required = true, value = "fileInput") @RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input file containing image scans") @Parameter(description = "The input file containing image scans")

@ -29,15 +29,17 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "Other", description = "Other APIs")
public class ExtractImagesController { public class ExtractImagesController {
private static final Logger logger = LoggerFactory.getLogger(ExtractImagesController.class); private static final Logger logger = LoggerFactory.getLogger(ExtractImagesController.class);
@PostMapping(consumes = "multipart/form-data", value = "/extract-images") @PostMapping(consumes = "multipart/form-data", value = "/extract-images")
@Operation(summary = "Extract images from a PDF file", @Operation(summary = "Extract images from a PDF file",
description = "This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format.") description = "This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input:PDF Output:IMAGE/ZIP Type:SIMO")
public ResponseEntity<byte[]> extractImages( public ResponseEntity<byte[]> extractImages(
@RequestPart(required = true, value = "fileInput") @RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file containing images") @Parameter(description = "The input PDF file containing images")

@ -1,12 +1,31 @@
package stirling.software.SPDF.controller.api.other; package stirling.software.SPDF.controller.api.other;
import java.awt.Color;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
//Required for image manipulation
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.awt.image.RescaleOp;
//Required for file input/output
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; //Other required classes
import java.nio.file.Path; import java.util.Random;
import java.util.ArrayList;
import java.util.List; //Required for image input/output
import javax.imageio.ImageIO;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -17,46 +36,17 @@ import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.io.source.ByteArrayOutputStream; import com.itextpdf.io.source.ByteArrayOutputStream;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import stirling.software.SPDF.utils.ProcessExecutor; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
//Required for PDF manipulation
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
//Required for image manipulation
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.RescaleOp;
import java.awt.image.AffineTransformOp;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.awt.Color;
import java.awt.geom.AffineTransform;
//Required for image input/output
import javax.imageio.ImageIO;
//Required for file input/output
import java.io.File;
//Other required classes
import java.util.Random;
import io.swagger.v3.oas.annotations.Hidden;
@RestController @RestController
public class FakeScanController { @Tag(name = "Other", description = "Other APIs")
public class FakeScanControllerWIP {
private static final Logger logger = LoggerFactory.getLogger(FakeScanController.class); private static final Logger logger = LoggerFactory.getLogger(FakeScanControllerWIP.class);
//TODO //TODO
@Hidden @Hidden

@ -19,9 +19,11 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "Other", description = "Other APIs")
public class MetadataController { public class MetadataController {
@ -38,7 +40,7 @@ public class MetadataController {
@PostMapping(consumes = "multipart/form-data", value = "/update-metadata") @PostMapping(consumes = "multipart/form-data", value = "/update-metadata")
@Operation(summary = "Update metadata of a PDF file", @Operation(summary = "Update metadata of a PDF file",
description = "This endpoint allows you to update the metadata of a given PDF file. You can add, modify, or delete standard and custom metadata fields.") description = "This endpoint allows you to update the metadata of a given PDF file. You can add, modify, or delete standard and custom metadata fields. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> metadata( public ResponseEntity<byte[]> metadata(
@RequestPart(required = true, value = "fileInput") @RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to update metadata") @Parameter(description = "The input PDF file to update metadata")
@ -73,6 +75,7 @@ public class MetadataController {
@RequestParam(value = "trapped", required = false) @RequestParam(value = "trapped", required = false)
@Parameter(description = "The trapped status of the document") @Parameter(description = "The trapped status of the document")
String trapped, String trapped,
@Parameter(description = "Map list of key and value of custom parameters, note these must start with customKey and customValue if they are non standard")
@RequestParam Map<String, String> allRequestParams) @RequestParam Map<String, String> allRequestParams)
throws IOException { throws IOException {

@ -27,10 +27,12 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "Other", description = "Other APIs")
public class OCRController { public class OCRController {
private static final Logger logger = LoggerFactory.getLogger(OCRController.class); private static final Logger logger = LoggerFactory.getLogger(OCRController.class);
@ -47,7 +49,7 @@ public class OCRController {
@PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf") @PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
@Operation(summary = "Process a PDF file with OCR", @Operation(summary = "Process a PDF file with OCR",
description = "This endpoint processes a PDF file using OCR (Optical Character Recognition). Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType, and removeImagesAfter options.") description = "This endpoint processes a PDF file using OCR (Optical Character Recognition). Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType, and removeImagesAfter options. Input:PDF Output:PDF Type:SI-Conditional")
public ResponseEntity<byte[]> processPdfWithOCR( public ResponseEntity<byte[]> processPdfWithOCR(
@RequestPart(required = true, value = "fileInput") @RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be processed with OCR") @Parameter(description = "The input PDF file to be processed with OCR")

@ -14,10 +14,12 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; 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.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "Other", description = "Other APIs")
public class OverlayImageController { public class OverlayImageController {
private static final Logger logger = LoggerFactory.getLogger(OverlayImageController.class); private static final Logger logger = LoggerFactory.getLogger(OverlayImageController.class);
@ -25,7 +27,7 @@ public class OverlayImageController {
@PostMapping(consumes = "multipart/form-data", value = "/add-image") @PostMapping(consumes = "multipart/form-data", value = "/add-image")
@Operation( @Operation(
summary = "Overlay image onto a PDF file", summary = "Overlay image onto a PDF file",
description = "This endpoint overlays an image onto a PDF file at the specified coordinates. The image can be overlaid on every page of the PDF if specified." description = "This endpoint overlays an image onto a PDF file at the specified coordinates. The image can be overlaid on every page of the PDF if specified. Input:PDF/IMAGE Output:PDF Type:MF-SISO"
) )
public ResponseEntity<byte[]> overlayImage( public ResponseEntity<byte[]> overlayImage(
@RequestPart(required = true, value = "fileInput") @RequestPart(required = true, value = "fileInput")

@ -16,10 +16,12 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "Other", description = "Other APIs")
public class RepairController { public class RepairController {
private static final Logger logger = LoggerFactory.getLogger(RepairController.class); private static final Logger logger = LoggerFactory.getLogger(RepairController.class);
@ -27,7 +29,7 @@ public class RepairController {
@PostMapping(consumes = "multipart/form-data", value = "/repair") @PostMapping(consumes = "multipart/form-data", value = "/repair")
@Operation( @Operation(
summary = "Repair a PDF file", summary = "Repair a PDF file",
description = "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response." description = "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response. Input:PDF Output:PDF Type:SISO"
) )
public ResponseEntity<byte[]> repairPdf( public ResponseEntity<byte[]> repairPdf(
@RequestPart(required = true, value = "fileInput") @RequestPart(required = true, value = "fileInput")

@ -0,0 +1,399 @@
package stirling.software.SPDF.controller.api.pipeline;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Pipeline", description = "Pipeline APIs")
public class Controller {
@Autowired
private ObjectMapper objectMapper;
final String jsonFileName = "pipelineCofig.json";
final String watchedFoldersDir = "watchedFolders/";
@Scheduled(fixedRate = 5000)
public void scanFolders() {
Path watchedFolderPath = Paths.get(watchedFoldersDir);
if (!Files.exists(watchedFolderPath)) {
try {
Files.createDirectories(watchedFolderPath);
} catch (IOException e) {
e.printStackTrace();
return;
}
}
try (Stream<Path> paths = Files.walk(watchedFolderPath)) {
paths.filter(Files::isDirectory).forEach(t -> {
try {
if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) {
handleDirectory(t);
}
} catch (Exception e) {
e.printStackTrace();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
private void handleDirectory(Path dir) throws Exception {
Path jsonFile = dir.resolve(jsonFileName);
Path processingDir = dir.resolve("processing"); // Directory to move files during processing
if (!Files.exists(processingDir)) {
Files.createDirectory(processingDir);
}
if (Files.exists(jsonFile)) {
// Read JSON file
String jsonString;
try {
jsonString = new String(Files.readAllBytes(jsonFile));
} catch (IOException e) {
e.printStackTrace();
return;
}
// Decode JSON to PipelineConfig
PipelineConfig config;
try {
config = objectMapper.readValue(jsonString, PipelineConfig.class);
// Assuming your PipelineConfig class has getters for all necessary fields, you can perform checks here
if (config.getOperations() == null || config.getOutputDir() == null || config.getName() == null) {
throw new IOException("Invalid JSON format");
}
} catch (IOException e) {
e.printStackTrace();
return;
}
// For each operation in the pipeline
for (PipelineOperation operation : config.getOperations()) {
// Collect all files based on fileInput
File[] files;
String fileInput = (String) operation.getParameters().get("fileInput");
if ("automated".equals(fileInput)) {
// If fileInput is "automated", process all files in the directory
try (Stream<Path> 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<Resource> 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<Resource> processFiles(List<Resource> outputFiles, String jsonString) throws Exception{
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(jsonString);
JsonNode pipelineNode = jsonNode.get("pipeline");
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
PrintStream logPrintStream = new PrintStream(logStream);
boolean hasErrors = false;
for (JsonNode operationNode : pipelineNode) {
String operation = operationNode.get("operation").asText();
JsonNode parametersNode = operationNode.get("parameters");
String inputFileExtension = "";
if(operationNode.has("inputFileType")) {
inputFileExtension = operationNode.get("inputFileType").asText();
} else {
inputFileExtension=".pdf";
}
List<Resource> newOutputFiles = new ArrayList<>();
boolean hasInputFileType = false;
for (Resource file : outputFiles) {
if (file.getFilename().endsWith(inputFileExtension)) {
hasInputFileType = true;
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("fileInput", file);
Iterator<Map.Entry<String, JsonNode>> parameters = parametersNode.fields();
while (parameters.hasNext()) {
Map.Entry<String, JsonNode> parameter = parameters.next();
body.add(parameter.getKey(), parameter.getValue().asText());
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
RestTemplate restTemplate = new RestTemplate();
String url = "http://localhost:8080/" + operation;
ResponseEntity<byte[]> response = restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
if (!response.getStatusCode().equals(HttpStatus.OK)) {
logPrintStream.println("Error: " + response.getBody());
hasErrors = true;
continue;
}
// Check if the response body is a zip file
if (isZip(response.getBody())) {
// Unzip the file and add all the files to the new output files
newOutputFiles.addAll(unzip(response.getBody()));
} else {
Resource outputResource = new ByteArrayResource(response.getBody()) {
@Override
public String getFilename() {
return file.getFilename(); // Preserving original filename
}
};
newOutputFiles.add(outputResource);
}
}
if (!hasInputFileType) {
logPrintStream.println("No files with extension " + inputFileExtension + " found for operation " + operation);
hasErrors = true;
}
outputFiles = newOutputFiles;
}
logPrintStream.close();
}
return outputFiles;
}
List<Resource> 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<Resource> 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<Resource> handleFiles(MultipartFile[] files, String jsonString) throws Exception{
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(jsonString);
JsonNode pipelineNode = jsonNode.get("pipeline");
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
PrintStream logPrintStream = new PrintStream(logStream);
boolean hasErrors = false;
List<Resource> outputFiles = new ArrayList<>();
for (MultipartFile file : files) {
Resource fileResource = new ByteArrayResource(file.getBytes()) {
@Override
public String getFilename() {
return file.getOriginalFilename();
}
};
outputFiles.add(fileResource);
}
return processFiles(outputFiles, jsonString);
}
@PostMapping("/handleData")
public ResponseEntity<byte[]> handleData(@RequestPart("fileInput") MultipartFile[] files,
@RequestParam("json") String jsonString) {
try {
List<Resource> outputFiles = handleFiles(files, jsonString);
if (outputFiles.size() == 1) {
// If there is only one file, return it directly
Resource singleFile = outputFiles.get(0);
InputStream is = singleFile.getInputStream();
byte[] bytes = new byte[(int)singleFile.contentLength()];
is.read(bytes);
is.close();
return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM);
}
// Create a ByteArrayOutputStream to hold the zip
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ZipOutputStream zipOut = new ZipOutputStream(baos);
// Loop through each file and add it to the zip
for (Resource file : outputFiles) {
ZipEntry zipEntry = new ZipEntry(file.getFilename());
zipOut.putNextEntry(zipEntry);
// Read the file into a byte array
InputStream is = file.getInputStream();
byte[] bytes = new byte[(int)file.contentLength()];
is.read(bytes);
// Write the bytes of the file to the zip
zipOut.write(bytes, 0, bytes.length);
zipOut.closeEntry();
is.close();
}
zipOut.close();
return WebResponseUtils.boasToWebResponse(baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private boolean isZip(byte[] data) {
if (data == null || data.length < 4) {
return false;
}
// Check the first four bytes of the data against the standard zip magic number
return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04;
}
private List<Resource> unzip(byte[] data) throws IOException {
List<Resource> unzippedFiles = new ArrayList<>();
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ZipInputStream zis = new ZipInputStream(bais)) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int count;
while ((count = zis.read(buffer)) != -1) {
baos.write(buffer, 0, count);
}
final String filename = entry.getName();
Resource fileResource = new ByteArrayResource(baos.toByteArray()) {
@Override
public String getFilename() {
return filename;
}
};
// If the unzipped file is a zip file, unzip it
if (isZip(baos.toByteArray())) {
unzippedFiles.addAll(unzip(baos.toByteArray()));
} else {
unzippedFiles.add(fileResource);
}
}
}
return unzippedFiles;
}
}

@ -51,8 +51,10 @@ import com.itextpdf.signatures.SignatureUtil;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "Security", description = "Security APIs")
public class CertSignController { public class CertSignController {
private static final Logger logger = LoggerFactory.getLogger(CertSignController.class); private static final Logger logger = LoggerFactory.getLogger(CertSignController.class);
@ -63,7 +65,7 @@ public class CertSignController {
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign") @PostMapping(consumes = "multipart/form-data", value = "/cert-sign")
@Operation(summary = "Sign PDF with a Digital Certificate", @Operation(summary = "Sign PDF with a Digital Certificate",
description = "This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file.") description = "This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:MF-SISO")
public ResponseEntity<byte[]> signPDF( public ResponseEntity<byte[]> signPDF(
@RequestPart(required = true, value = "fileInput") @RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to be signed") @Parameter(description = "The input PDF file to be signed")

@ -17,8 +17,10 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "Security", description = "Security APIs")
public class PasswordController { public class PasswordController {
private static final Logger logger = LoggerFactory.getLogger(PasswordController.class); private static final Logger logger = LoggerFactory.getLogger(PasswordController.class);
@ -27,7 +29,7 @@ public class PasswordController {
@PostMapping(consumes = "multipart/form-data", value = "/remove-password") @PostMapping(consumes = "multipart/form-data", value = "/remove-password")
@Operation( @Operation(
summary = "Remove password from a PDF file", summary = "Remove password from a PDF file",
description = "This endpoint removes the password from a protected PDF file. Users need to provide the existing password." description = "This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO"
) )
public ResponseEntity<byte[]> removePassword( public ResponseEntity<byte[]> removePassword(
@RequestPart(required = true, value = "fileInput") @RequestPart(required = true, value = "fileInput")
@ -44,7 +46,7 @@ public class PasswordController {
@PostMapping(consumes = "multipart/form-data", value = "/add-password") @PostMapping(consumes = "multipart/form-data", value = "/add-password")
@Operation( @Operation(
summary = "Add password to a PDF file", summary = "Add password to a PDF file",
description = "This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file." description = "This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF"
) )
public ResponseEntity<byte[]> addPassword( public ResponseEntity<byte[]> addPassword(
@RequestPart(required = true, value = "fileInput") @RequestPart(required = true, value = "fileInput")

@ -1,15 +1,23 @@
package stirling.software.SPDF.controller.api.security; package stirling.software.SPDF.controller.api.security;
import java.awt.Color; import java.awt.Color;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDFont; 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.font.PDType1Font;
import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
import org.apache.pdfbox.util.Matrix; import org.apache.pdfbox.util.Matrix;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
@ -19,18 +27,26 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
import io.swagger.v3.oas.annotations.media.Schema;
@RestController @RestController
@Tag(name = "Security", description = "Security APIs")
public class WatermarkController { public class WatermarkController {
@PostMapping(consumes = "multipart/form-data", value = "/add-watermark") @PostMapping(consumes = "multipart/form-data", value = "/add-watermark")
@Operation(summary = "Add watermark to a PDF file", @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.") 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<byte[]> addWatermark( public ResponseEntity<byte[]> addWatermark(
@RequestPart(required = true, value = "fileInput") @RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to add a watermark") @Parameter(description = "The input PDF file to add a watermark")
MultipartFile pdfFile, 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") @RequestParam("watermarkText")
@Parameter(description = "The watermark text to add to the PDF file") @Parameter(description = "The watermark text to add to the PDF file")
String watermarkText, String watermarkText,
@ -48,11 +64,11 @@ public class WatermarkController {
int widthSpacer, int widthSpacer,
@RequestParam(defaultValue = "50", name = "heightSpacer") @RequestParam(defaultValue = "50", name = "heightSpacer")
@Parameter(description = "The height spacer between watermark texts", example = "50") @Parameter(description = "The height spacer between watermark texts", example = "50")
int heightSpacer) throws IOException { int heightSpacer) throws IOException, Exception {
// Load the input PDF // Load the input PDF
PDDocument document = PDDocument.load(pdfFile.getInputStream()); PDDocument document = PDDocument.load(pdfFile.getInputStream());
String producer = document.getDocumentInformation().getProducer();
// Create a page in the document // Create a page in the document
for (PDPage page : document.getPages()) { for (PDPage page : document.getPages()) {
@ -64,8 +80,40 @@ public class WatermarkController {
graphicsState.setNonStrokingAlphaConstant(opacity); graphicsState.setNonStrokingAlphaConstant(opacity);
contentStream.setGraphicsStateParameters(graphicsState); contentStream.setGraphicsStateParameters(graphicsState);
// Set font of watermark
String resourceDir = "";
PDFont font = PDType1Font.HELVETICA_BOLD; 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.beginText(); contentStream.beginText();
contentStream.setFont(font, fontSize); contentStream.setFont(font, fontSize);
contentStream.setNonStrokingColor(Color.LIGHT_GRAY); contentStream.setNonStrokingColor(Color.LIGHT_GRAY);
@ -81,11 +129,19 @@ public class WatermarkController {
// Add the watermark text // Add the watermark text
for (int i = 0; i < watermarkRows; i++) { for (int i = 0; i < watermarkRows; i++) {
for (int j = 0; j < watermarkCols; j++) { 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.setTextMatrix(Matrix.getRotateInstance((float) Math.toRadians(rotation), j * watermarkWidth, i * watermarkHeight));
}
contentStream.showTextWithPositioning(new Object[] { watermarkText }); contentStream.showTextWithPositioning(new Object[] { watermarkText });
} }
} }
contentStream.endText(); contentStream.endText();
// Close the content stream // Close the content stream

@ -6,8 +6,10 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
@Controller @Controller
@Tag(name = "Convert", description = "Convert APIs")
public class ConverterWebController { public class ConverterWebController {
@GetMapping("/img-to-pdf") @GetMapping("/img-to-pdf")

@ -1,27 +1,29 @@
package stirling.software.SPDF.controller.web; package stirling.software.SPDF.controller.web;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
@Controller @Controller
@Tag(name = "General", description = "General APIs")
public class GeneralWebController { public class GeneralWebController {
@GetMapping("/pipeline")
@Hidden
public String pipelineForm(Model model) {
model.addAttribute("currentPage", "pipeline");
return "pipeline";
}
@GetMapping("/merge-pdfs") @GetMapping("/merge-pdfs")
@Hidden @Hidden
public String mergePdfForm(Model model) { public String mergePdfForm(Model model) {
model.addAttribute("currentPage", "merge-pdfs"); model.addAttribute("currentPage", "merge-pdfs");
return "merge-pdfs"; return "merge-pdfs";
} }
@GetMapping("/about")
@Hidden
public String gameForm(Model model) {
model.addAttribute("currentPage", "about");
return "about";
}
@GetMapping("/multi-tool") @GetMapping("/multi-tool")
@Hidden @Hidden
@ -30,16 +32,6 @@ public class GeneralWebController {
return "multi-tool"; return "multi-tool";
} }
@GetMapping("/")
public String home(Model model) {
model.addAttribute("currentPage", "home");
return "home";
}
@GetMapping("/home")
public String root(Model model) {
return "redirect:/";
}
@GetMapping("/remove-pages") @GetMapping("/remove-pages")
@Hidden @Hidden
@ -76,20 +68,4 @@ public class GeneralWebController {
return "sign"; return "sign";
} }
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody
@Hidden
public String getRobotsTxt() {
String allowGoogleVisibility = System.getProperty("ALLOW_GOOGLE_VISIBILITY");
if (allowGoogleVisibility == null)
allowGoogleVisibility = System.getenv("ALLOW_GOOGLE_VISIBILITY");
if (allowGoogleVisibility == null)
allowGoogleVisibility = "false";
if (Boolean.parseBoolean(allowGoogleVisibility)) {
return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /";
} else {
return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /";
}
}
} }

@ -0,0 +1,52 @@
package stirling.software.SPDF.controller.web;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import io.swagger.v3.oas.annotations.Hidden;
@Controller
public class HomeWebController {
@GetMapping("/about")
@Hidden
public String gameForm(Model model) {
model.addAttribute("currentPage", "about");
return "about";
}
@GetMapping("/")
public String home(Model model) {
model.addAttribute("currentPage", "home");
return "home";
}
@GetMapping("/home")
public String root(Model model) {
return "redirect:/";
}
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody
@Hidden
public String getRobotsTxt() {
String allowGoogleVisibility = System.getProperty("ALLOW_GOOGLE_VISIBILITY");
if (allowGoogleVisibility == null)
allowGoogleVisibility = System.getenv("ALLOW_GOOGLE_VISIBILITY");
if (allowGoogleVisibility == null)
allowGoogleVisibility = "false";
if (Boolean.parseBoolean(allowGoogleVisibility)) {
return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /";
} else {
return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /";
}
}
}

@ -12,9 +12,12 @@ import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.MeterRegistry;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@RestController @RestController
@RequestMapping("/api/v1") @RequestMapping("/api/v1")
@Tag(name = "API", description = "Info APIs")
public class MetricsController { public class MetricsController {
private final MeterRegistry meterRegistry; private final MeterRegistry meterRegistry;
@ -36,18 +39,32 @@ public class MetricsController {
@GetMapping("/loads") @GetMapping("/loads")
@Operation(summary = "GET request count", @Operation(summary = "GET request count",
description = "This endpoint returns the total count of GET requests or the count of GET requests for a specific endpoint.") description = "This endpoint returns the total count of GET requests or the count of GET requests for a specific endpoint.")
public Double getPageLoads(@RequestParam Optional<String> endpoint) { public Double getPageLoads(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional<String> endpoint) {
try { try {
double count = 0.0; double count = 0.0;
for (Meter meter : meterRegistry.getMeters()) { for (Meter meter : meterRegistry.getMeters()) {
if (meter.getId().getName().equals("http.requests")) { if (meter.getId().getName().equals("http.requests")) {
String method = meter.getId().getTag("method"); String method = meter.getId().getTag("method");
if (method != null && method.equals("GET")) { if (method != null && method.equals("GET")) {
if (endpoint.isPresent() && !endpoint.get().isBlank()) {
if(!endpoint.get().startsWith("/")) {
endpoint = Optional.of("/" + endpoint.get());
}
System.out.println("loads " + endpoint.get() + " vs " + meter.getId().getTag("uri"));
if(endpoint.get().equals(meter.getId().getTag("uri"))){
if (meter instanceof Counter) { if (meter instanceof Counter) {
count += ((Counter) meter).count(); count += ((Counter) meter).count();
} }
} }
} else {
if (meter instanceof Counter) {
count += ((Counter) meter).count();
}
}
}
} }
} }
@ -60,10 +77,15 @@ public class MetricsController {
@GetMapping("/requests") @GetMapping("/requests")
@Operation(summary = "POST request count", @Operation(summary = "POST request count",
description = "This endpoint returns the total count of POST requests or the count of POST requests for a specific endpoint.") description = "This endpoint returns the total count of POST requests or the count of POST requests for a specific endpoint.")
public Double getTotalRequests(@RequestParam Optional<String> endpoint) { public Double getTotalRequests(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional<String> endpoint) {
try { try {
Counter counter; Counter counter;
if (endpoint.isPresent()) { if (endpoint.isPresent() && !endpoint.get().isBlank()) {
if(!endpoint.get().startsWith("/")) {
endpoint = Optional.of("/" + endpoint.get());
}
System.out.println("loads " + endpoint.get() + " vs " + meterRegistry.get("http.requests").tags("uri", endpoint.get()).toString());
counter = meterRegistry.get("http.requests") counter = meterRegistry.get("http.requests")
.tags("method", "POST", "uri", endpoint.get()).counter(); .tags("method", "POST", "uri", endpoint.get()).counter();
} else { } else {
@ -72,7 +94,8 @@ public class MetricsController {
} }
return counter.count(); return counter.count();
} catch (Exception e) { } catch (Exception e) {
return -1.0; e.printStackTrace();
return 0.0;
} }
} }

@ -12,8 +12,10 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
@Controller @Controller
@Tag(name = "Other", description = "Other APIs")
public class OtherWebController { public class OtherWebController {
@GetMapping("/compress-pdf") @GetMapping("/compress-pdf")
@Hidden @Hidden

@ -5,8 +5,10 @@ import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
@Controller @Controller
@Tag(name = "Security", description = "Security APIs")
public class SecurityWebController { public class SecurityWebController {
@GetMapping("/add-password") @GetMapping("/add-password")
@Hidden @Hidden

@ -0,0 +1,51 @@
package stirling.software.SPDF.model;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
public class PipelineConfig {
private String name;
@JsonProperty("pipeline")
private List<PipelineOperation> operations;
private String outputDir;
@JsonProperty("outputFileName")
private String outputPattern;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<PipelineOperation> getOperations() {
return operations;
}
public void setOperations(List<PipelineOperation> operations) {
this.operations = operations;
}
public String getOutputDir() {
return outputDir;
}
public void setOutputDir(String outputDir) {
this.outputDir = outputDir;
}
public String getOutputPattern() {
return outputPattern;
}
public void setOutputPattern(String outputPattern) {
this.outputPattern = outputPattern;
}
}

@ -0,0 +1,25 @@
package stirling.software.SPDF.model;
import java.util.Map;
public class PipelineOperation {
private String operation;
private Map<String, Object> parameters;
public String getOperation() {
return operation;
}
public void setOperation(String operation) {
this.operation = operation;
}
public Map<String, Object> getParameters() {
return parameters;
}
public void setParameters(Map<String, Object> parameters) {
this.parameters = parameters;
}
}

@ -27,14 +27,179 @@ import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.rendering.ImageType; import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.text.PDFTextStripper;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.canvas.parser.PdfTextExtractor;
import com.itextpdf.kernel.pdf.canvas.parser.listener.SimpleTextExtractionStrategy;
import stirling.software.SPDF.pdf.ImageFinder;
public class PdfUtils { public class PdfUtils {
private static final Logger logger = LoggerFactory.getLogger(PdfUtils.class); private static final Logger logger = LoggerFactory.getLogger(PdfUtils.class);
public static PDRectangle textToPageSize(String size) {
switch (size) {
case "A0":
return PDRectangle.A0;
case "A1":
return PDRectangle.A1;
case "A2":
return PDRectangle.A2;
case "A3":
return PDRectangle.A3;
case "A4":
return PDRectangle.A4;
case "A5":
return PDRectangle.A5;
case "A6":
return PDRectangle.A6;
case "LETTER":
return PDRectangle.LETTER;
case "LEGAL":
return PDRectangle.LEGAL;
default:
throw new IllegalArgumentException("Invalid standard page size: " + size);
}
}
public 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+", "");
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);
}
}
}
pdfDocument.close();
return pdfText.contains(text);
}
public static boolean hasImagesOnPage(PDPage page) throws IOException {
ImageFinder imageFinder = new ImageFinder(page);
imageFinder.processPage(page);
return imageFinder.hasImages();
}
public static boolean hasText(PDDocument document, String phrase) throws IOException {
PDFTextStripper pdfStripper = new PDFTextStripper();
String text = pdfStripper.getText(document);
return text.contains(phrase);
}
public boolean containsTextInFile(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+", "");
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);
}
}
}
pdfDocument.close();
return pdfText.contains(text);
}
public boolean pageCount(PDDocument pdfDocument, int pageCount, String comparator) throws IOException {
int actualPageCount = pdfDocument.getNumberOfPages();
pdfDocument.close();
switch(comparator.toLowerCase()) {
case "greater":
return actualPageCount > pageCount;
case "equal":
return actualPageCount == pageCount;
case "less":
return actualPageCount < pageCount;
default:
throw new IllegalArgumentException("Invalid comparator. Only 'greater', 'equal', and 'less' are supported.");
}
}
public boolean pageSize(PDDocument pdfDocument, String expectedPageSize) throws IOException {
PDPage firstPage = pdfDocument.getPage(0);
PDRectangle mediaBox = firstPage.getMediaBox();
float actualPageWidth = mediaBox.getWidth();
float actualPageHeight = mediaBox.getHeight();
pdfDocument.close();
// Assumes the expectedPageSize is in the format "widthxheight", e.g. "595x842" for A4
String[] dimensions = expectedPageSize.split("x");
float expectedPageWidth = Float.parseFloat(dimensions[0]);
float expectedPageHeight = Float.parseFloat(dimensions[1]);
// Checks if the actual page size matches the expected page size
return actualPageWidth == expectedPageWidth && actualPageHeight == expectedPageHeight;
}
public static byte[] convertFromPdf(byte[] inputStream, String imageType, ImageType colorType, boolean singleImage, int DPI, String filename) throws IOException, Exception { public static byte[] convertFromPdf(byte[] inputStream, String imageType, ImageType colorType, boolean singleImage, int DPI, String filename) throws IOException, Exception {
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(inputStream))) { try (PDDocument document = PDDocument.load(new ByteArrayInputStream(inputStream))) {
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);
@ -43,7 +208,7 @@ public class PdfUtils {
// Create images of all pages // Create images of all pages
for (int i = 0; i < pageCount; i++) { for (int i = 0; i < pageCount; i++) {
images.add(pdfRenderer.renderImageWithDPI(i, 300, colorType)); images.add(pdfRenderer.renderImageWithDPI(i, DPI, colorType));
} }
if (singleImage) { if (singleImage) {

@ -1,12 +1,12 @@
spring.http.multipart.max-file-size=2GB spring.http.multipart.max-file-size=${MAX_FILE_SIZE:2000MB}
spring.http.multipart.max-request-size=2GB spring.http.multipart.max-request-size=${MAX_FILE_SIZE:2000MB}
multipart.enabled=true multipart.enabled=true
multipart.max-file-size=2000MB multipart.max-file-size=${MAX_FILE_SIZE:2000MB}
multipart.max-request-size=2000MB multipart.max-request-size=${MAX_FILE_SIZE:2000MB}
spring.servlet.multipart.max-file-size=2000MB spring.servlet.multipart.max-file-size=${MAX_FILE_SIZE:2000MB}
spring.servlet.multipart.max-request-size=2000MB spring.servlet.multipart.max-request-size=${MAX_FILE_SIZE:2000MB}
server.forward-headers-strategy=NATIVE server.forward-headers-strategy=NATIVE

@ -24,7 +24,7 @@ close=\u0625\u063A\u0644\u0627\u0642
filesSelected = الملفات المحددة filesSelected = الملفات المحددة
noFavourites = لم تتم إضافة أي مفضلات noFavourites = لم تتم إضافة أي مفضلات
bored = الانتظار بالملل؟ bored = الانتظار بالملل؟
alphabet=\u0627\u0644\u0623\u0628\u062C\u062F\u064A\u0629
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############

@ -20,6 +20,7 @@ close=Tanca
filesSelected=fitxers seleccionats filesSelected=fitxers seleccionats
noFavourites=No s'ha afegit cap favorit noFavourites=No s'ha afegit cap favorit
bored=Avorrit esperant? bored=Avorrit esperant?
alphabet=Alfabet
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############

@ -20,6 +20,7 @@ close=Schließen
filesSelected=Dateien ausgewählt filesSelected=Dateien ausgewählt
noFavourites=Keine Favoriten hinzugefügt noFavourites=Keine Favoriten hinzugefügt
bored=Gelangweiltes Warten? bored=Gelangweiltes Warten?
alphabet=Alphabet
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############

@ -20,6 +20,7 @@ close=Close
filesSelected=files selected filesSelected=files selected
noFavourites=No favourites added noFavourites=No favourites added
bored=Bored Waiting? bored=Bored Waiting?
alphabet=Alphabet
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############
@ -131,6 +132,9 @@ home.pageLayout.desc=Merge multiple pages of a PDF document into a single page
home.scalePages.title=Adjust page size/scale home.scalePages.title=Adjust page size/scale
home.scalePages.desc=Change the size/scale of a page and/or its contents. home.scalePages.desc=Change the size/scale of a page and/or its contents.
home.pipeline.title=Pipeline
home.pipeline.desc=Pipeline desc.
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect
downloadPdf=Download PDF downloadPdf=Download PDF
@ -139,6 +143,8 @@ font=Font
selectFillter=-- Select -- selectFillter=-- Select --
pageNum=Page Number pageNum=Page Number
pipeline.title=Pipeline
pageLayout.title=Multi Page Layout pageLayout.title=Multi Page Layout
pageLayout.header=Multi Page Layout pageLayout.header=Multi Page Layout
pageLayout.pagesPerSheet=Pages per sheet: pageLayout.pagesPerSheet=Pages per sheet:

@ -20,6 +20,7 @@ close=Cerrar
filesSelected=archivos seleccionados filesSelected=archivos seleccionados
noFavourites=No se agregaron favoritos noFavourites=No se agregaron favoritos
bored=¿Aburrido de esperar? bored=¿Aburrido de esperar?
alphabet=Alfabeto
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############

@ -20,6 +20,7 @@ close=Itxi
filesSelected=Hautatutako fitxategiak filesSelected=Hautatutako fitxategiak
noFavourites=Ez dira gogokoak gehitu noFavourites=Ez dira gogokoak gehitu
bored=Itxaroten aspertuta? bored=Itxaroten aspertuta?
alphabet=Alfabetoa
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############

@ -24,6 +24,7 @@ close=Fermer
filesSelected=fichiers sélectionnés filesSelected=fichiers sélectionnés
noFavourites=Aucun favori ajouté noFavourites=Aucun favori ajouté
bored=Ennuyé d'attendre ? bored=Ennuyé d'attendre ?
alphabet=Alphabet
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############

@ -20,6 +20,7 @@ close=Chiudi
filesSelected=file selezionati filesSelected=file selezionati
noFavourites=Nessun preferito noFavourites=Nessun preferito
bored=Stanco di aspettare? bored=Stanco di aspettare?
alphabet=Alfabeto
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############

@ -19,7 +19,8 @@ save=保存
close=閉じる close=閉じる
filesSelected=選択されたファイル filesSelected=選択されたファイル
noFavourites=お気に入りはありません noFavourites=お気に入りはありません
bored=待ち時間が退屈? bored=å¾…ã<EFBFBD>¡æ™é“ã<EFBFBD>Œé€€å±ˆï¼
alphabet=\u30A2\u30EB\u30D5\u30A1\u30D9\u30C3\u30C8Ÿ
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############

@ -20,6 +20,7 @@ close=닫기
filesSelected=개 파일 선택됨 filesSelected=개 파일 선택됨
noFavourites=즐겨찾기 없음 noFavourites=즐겨찾기 없음
bored=기다리는 게 지루하신가요? bored=기다리는 게 지루하신가요?
alphabet=\uC54C\uD30C\uBCB3
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############

@ -20,6 +20,7 @@ close=Zamknij
filesSelected=wybrane pliki filesSelected=wybrane pliki
noFavourites=Nie dodano ulubionych noFavourites=Nie dodano ulubionych
bored=Znudzony czekaniem? bored=Znudzony czekaniem?
alphabet=Alfabet
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############

@ -20,6 +20,7 @@ close=Fechar
filesSelected=arquivos selecionados filesSelected=arquivos selecionados
noFavourites=Nenhum favorito adicionado noFavourites=Nenhum favorito adicionado
bored=Entediado esperando? bored=Entediado esperando?
alphabet=Alfabeto
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############

@ -20,6 +20,7 @@ close=Închide
filesSelected=fișiere selectate filesSelected=fișiere selectate
noFavourites=Niciun favorit adăugat noFavourites=Niciun favorit adăugat
bored=Plictisit așteptând? bored=Plictisit așteptând?
alphabet=Alfabet
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############

@ -20,6 +20,7 @@ close=Закрыть
filesSelected=файлов выбрано filesSelected=файлов выбрано
noFavourites=Нет избранного noFavourites=Нет избранного
bored=Скучно ждать? bored=Скучно ждать?
alphabet=\u0430\u043B\u0444\u0430\u0432\u0438\u0442
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############

@ -20,6 +20,7 @@ close=Stäng
filesSelected=filer valda filesSelected=filer valda
noFavourites=Inga favoriter har lagts till noFavourites=Inga favoriter har lagts till
bored=Utråkad att vänta? bored=Utråkad att vänta?
alphabet=Alfabet
############# #############
# HEMSIDA # # HEMSIDA #
############# #############

@ -20,6 +20,7 @@ close=关闭
filesSelected=\u9009\u62E9\u7684\u6587\u4EF6 filesSelected=\u9009\u62E9\u7684\u6587\u4EF6
noFavourites=\u6CA1\u6709\u6DFB\u52A0\u6536\u85CF\u5939 noFavourites=\u6CA1\u6709\u6DFB\u52A0\u6536\u85CF\u5939
bored=\u65E0\u804A\u7B49\u5F85\uFF1F bored=\u65E0\u804A\u7B49\u5F85\uFF1F
alphabet=\u5B57\u6BCD\u8868
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-bezier2" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1 2.5A1.5 1.5 0 0 1 2.5 1h1A1.5 1.5 0 0 1 5 2.5h4.134a1 1 0 1 1 0 1h-2.01c.18.18.34.381.484.605.638.992.892 2.354.892 3.895 0 1.993.257 3.092.713 3.7.356.476.895.721 1.787.784A1.5 1.5 0 0 1 12.5 11h1a1.5 1.5 0 0 1 1.5 1.5v1a1.5 1.5 0 0 1-1.5 1.5h-1a1.5 1.5 0 0 1-1.5-1.5H6.866a1 1 0 1 1 0-1h1.711a2.839 2.839 0 0 1-.165-.2C7.743 11.407 7.5 10.007 7.5 8c0-1.46-.246-2.597-.733-3.355-.39-.605-.952-1-1.767-1.112A1.5 1.5 0 0 1 3.5 5h-1A1.5 1.5 0 0 1 1 3.5v-1zM2.5 2a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1zm10 10a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1z"/>
</svg>

After

(image error) Size: 790 B

@ -218,7 +218,7 @@ const DraggableUtils = {
}, },
async getOverlayedPdfDocument() { async getOverlayedPdfDocument() {
const pdfBytes = await this.pdfDoc.getData(); const pdfBytes = await this.pdfDoc.getData();
const pdfDocModified = await PDFLib.PDFDocument.load(pdfBytes); const pdfDocModified = await PDFLib.PDFDocument.load(pdfBytes, { ignoreEncryption: true });
this.storePageContents(); this.storePageContents();
const pagesMap = this.documentsMap.get(this.pdfDoc); const pagesMap = this.documentsMap.get(this.pdfDoc);

@ -152,7 +152,7 @@ class PdfContainer {
async toPdfLib(objectUrl) { async toPdfLib(objectUrl) {
const existingPdfBytes = await fetch(objectUrl).then(res => res.arrayBuffer()); const existingPdfBytes = await fetch(objectUrl).then(res => res.arrayBuffer());
const pdfDoc = await PDFLib.PDFDocument.load(existingPdfBytes); const pdfDoc = await PDFLib.PDFDocument.load(existingPdfBytes, { ignoreEncryption: true });
return pdfDoc; return pdfDoc;
} }

@ -0,0 +1,419 @@
document.getElementById('validateButton').addEventListener('click', function(event) {
event.preventDefault();
validatePipeline();
});
function validatePipeline() {
let pipelineListItems = document.getElementById('pipelineList').children;
let isValid = true;
let containsAddPassword = false;
for (let i = 0; i < pipelineListItems.length - 1; i++) {
let currentOperation = pipelineListItems[i].querySelector('.operationName').textContent;
let nextOperation = pipelineListItems[i + 1].querySelector('.operationName').textContent;
if (currentOperation === '/add-password') {
containsAddPassword = true;
}
console.log(currentOperation);
console.log(apiDocs[currentOperation]);
let currentOperationDescription = apiDocs[currentOperation]?.post?.description || "";
let nextOperationDescription = apiDocs[nextOperation]?.post?.description || "";
console.log("currentOperationDescription", currentOperationDescription);
console.log("nextOperationDescription", nextOperationDescription);
let currentOperationOutput = currentOperationDescription.match(/Output:([A-Z\/]*)/)?.[1] || "";
let nextOperationInput = nextOperationDescription.match(/Input:([A-Z\/]*)/)?.[1] || "";
console.log("Operation " + currentOperation + " Output: " + currentOperationOutput);
console.log("Operation " + nextOperation + " Input: " + nextOperationInput);
// Splitting in case of multiple possible output/input
let currentOperationOutputArr = currentOperationOutput.split('/');
let nextOperationInputArr = nextOperationInput.split('/');
if (currentOperationOutput !== 'ANY' && nextOperationInput !== 'ANY') {
let intersection = currentOperationOutputArr.filter(value => nextOperationInputArr.includes(value));
console.log(`Intersection: ${intersection}`);
if (intersection.length === 0) {
isValid = false;
console.log(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`);
alert(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`);
break;
}
}
}
if (containsAddPassword && pipelineListItems[pipelineListItems.length - 1].querySelector('.operationName').textContent !== '/add-password') {
alert('The "add-password" operation should be at the end of the operations sequence. Please adjust the operations order.');
return false;
}
if (isValid) {
console.log('Pipeline is valid');
// Continue with the pipeline operation
} else {
console.error('Pipeline is not valid');
// Stop operation, maybe display an error to the user
}
return isValid;
}
document.getElementById('submitConfigBtn').addEventListener('click', function() {
if (validatePipeline() === false) {
return;
}
let selectedOperation = document.getElementById('operationsDropdown').value;
let parameters = operationSettings[selectedOperation] || {};
let pipelineConfig = {
"name": "uniquePipelineName",
"pipeline": [{
"operation": selectedOperation,
"parameters": parameters
}]
};
let pipelineConfigJson = JSON.stringify(pipelineConfig, null, 2);
let formData = new FormData();
let fileInput = document.getElementById('fileInput');
let files = fileInput.files;
for (let i = 0; i < files.length; i++) {
console.log("files[i]", files[i].name);
formData.append('fileInput', files[i], files[i].name);
}
console.log("pipelineConfigJson", pipelineConfigJson);
formData.append('json', pipelineConfigJson);
console.log("formData", formData);
fetch('/handleData', {
method: 'POST',
body: formData
})
.then(response => response.blob())
.then(blob => {
let url = window.URL.createObjectURL(blob);
let a = document.createElement('a');
a.href = url;
a.download = 'outputfile';
document.body.appendChild(a);
a.click();
a.remove();
})
.catch((error) => {
console.error('Error:', error);
});
});
let apiDocs = {};
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 = {};
// Group operations by tags
Object.keys(data.paths).forEach(operationPath => {
let operation = data.paths[operationPath].post;
if (operation && !ignoreOperations.includes(operationPath) && !operation.description.includes("Type:MISO")) {
let operationTag = operation.tags[0]; // This assumes each operation has exactly one tag
if (!operationsByTag[operationTag]) {
operationsByTag[operationTag] = [];
}
operationsByTag[operationTag].push(operationPath);
}
});
// Specify the order of tags
let tagOrder = ["General", "Security", "Convert", "Other", "Filter"];
// Create dropdown options
tagOrder.forEach(tag => {
if (operationsByTag[tag]) {
let group = document.createElement('optgroup');
group.label = tag;
operationsByTag[tag].forEach(operationPath => {
let option = document.createElement('option');
let 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);
}
});
});
document.getElementById('addOperationBtn').addEventListener('click', function() {
let selectedOperation = document.getElementById('operationsDropdown').value;
let pipelineList = document.getElementById('pipelineList');
let listItem = document.createElement('li');
listItem.className = "list-group-item";
let hasSettings = (apiDocs[selectedOperation] && apiDocs[selectedOperation].post &&
apiDocs[selectedOperation].post.parameters && apiDocs[selectedOperation].post.parameters.length > 0);
listItem.innerHTML = `
<div class="d-flex justify-content-between align-items-center w-100">
<div class="operationName">${selectedOperation}</div>
<div class="arrows d-flex">
<button class="btn btn-secondary move-up btn-margin"><span>&uarr;</span></button>
<button class="btn btn-secondary move-down btn-margin"><span>&darr;</span></button>
<button class="btn btn-warning pipelineSettings btn-margin" ${hasSettings ? "" : "disabled"}><span style="color: ${hasSettings ? "black" : "grey"};"></span></button>
<button class="btn btn-danger remove"><span>X</span></button>
</div>
</div>
`;
pipelineList.appendChild(listItem);
listItem.querySelector('.move-up').addEventListener('click', function(event) {
event.preventDefault();
if (listItem.previousElementSibling) {
pipelineList.insertBefore(listItem, listItem.previousElementSibling);
}
});
listItem.querySelector('.move-down').addEventListener('click', function(event) {
event.preventDefault();
if (listItem.nextElementSibling) {
pipelineList.insertBefore(listItem.nextElementSibling, listItem);
}
});
listItem.querySelector('.remove').addEventListener('click', function(event) {
event.preventDefault();
pipelineList.removeChild(listItem);
});
listItem.querySelector('.pipelineSettings').addEventListener('click', function(event) {
event.preventDefault();
showpipelineSettingsModal(selectedOperation);
});
function showpipelineSettingsModal(operation) {
let pipelineSettingsModal = document.getElementById('pipelineSettingsModal');
let pipelineSettingsContent = document.getElementById('pipelineSettingsContent');
let operationData = apiDocs[operation].post.parameters || [];
pipelineSettingsContent.innerHTML = '';
operationData.forEach(parameter => {
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 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";
}
parameterInput.id = parameter.name;
if (operationSettings[operation] && operationSettings[operation][parameter.name] !== undefined) {
let savedValue = operationSettings[operation][parameter.name];
switch (parameter.schema.type) {
case 'number':
case 'integer':
parameterInput.value = savedValue.toString();
break;
case 'boolean':
parameterInput.checked = savedValue;
break;
case 'array':
case 'object':
parameterInput.value = JSON.stringify(savedValue);
break;
default:
parameterInput.value = savedValue;
}
}
parameterDiv.appendChild(parameterInput);
pipelineSettingsContent.appendChild(parameterDiv);
});
let saveButton = document.createElement('button');
saveButton.textContent = "Save Settings";
saveButton.className = "btn btn-primary";
saveButton.addEventListener('click', function(event) {
event.preventDefault();
let settings = {};
operationData.forEach(parameter => {
let value = document.getElementById(parameter.name).value;
switch (parameter.schema.type) {
case 'number':
case 'integer':
settings[parameter.name] = Number(value);
break;
case 'boolean':
settings[parameter.name] = document.getElementById(parameter.name).checked;
break;
case 'array':
case 'object':
try {
settings[parameter.name] = JSON.parse(value);
} catch (err) {
console.error(`Invalid JSON format for ${parameter.name}`);
}
break;
default:
settings[parameter.name] = value;
}
});
operationSettings[operation] = settings;
console.log(settings);
pipelineSettingsModal.style.display = "none";
});
pipelineSettingsContent.appendChild(saveButton);
pipelineSettingsModal.style.display = "block";
pipelineSettingsModal.getElementsByClassName("close")[0].onclick = function() {
pipelineSettingsModal.style.display = "none";
}
window.onclick = function(event) {
if (event.target == pipelineSettingsModal) {
pipelineSettingsModal.style.display = "none";
}
}
}
document.getElementById('savePipelineBtn').addEventListener('click', function() {
if (validatePipeline() === false) {
return;
}
let pipelineList = document.getElementById('pipelineList').children;
let pipelineConfig = {
"name": "uniquePipelineName",
"pipeline": []
};
for (let i = 0; i < pipelineList.length; i++) {
let operationName = pipelineList[i].querySelector('.operationName').textContent;
let parameters = operationSettings[operationName] || {};
pipelineConfig.pipeline.push({
"operation": operationName,
"parameters": parameters
});
}
let a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([JSON.stringify(pipelineConfig, null, 2)], {
type: 'application/json'
}));
a.download = 'pipelineConfig.json';
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
document.getElementById('uploadPipelineBtn').addEventListener('click', function() {
document.getElementById('uploadPipelineInput').click();
});
document.getElementById('uploadPipelineInput').addEventListener('change', function(e) {
let reader = new FileReader();
reader.onload = function(event) {
let pipelineConfig = JSON.parse(event.target.result);
let pipelineList = document.getElementById('pipelineList');
while (pipelineList.firstChild) {
pipelineList.removeChild(pipelineList.firstChild);
}
pipelineConfig.pipeline.forEach(operationConfig => {
let operationsDropdown = document.getElementById('operationsDropdown');
operationsDropdown.value = operationConfig.operation;
operationSettings[operationConfig.operation] = operationConfig.parameters;
document.getElementById('addOperationBtn').click();
let lastOperation = pipelineList.lastChild;
lastOperation.querySelector('.pipelineSettings').click();
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 'text':
case 'textarea':
default:
input.value = JSON.stringify(operationConfig.parameters[parameterName]);
}
}
});
document.querySelector('#pipelineSettingsModal .btn-primary').click();
});
};
reader.readAsText(e.target.files[0]);
});
});

@ -32,6 +32,13 @@
<span class="icon-text" th:text="#{home.multiTool.title}"></span> <span class="icon-text" th:text="#{home.multiTool.title}"></span>
</a> </a>
</li> </li>
<!--<li class="nav-item">
<a class="nav-link" href="#" th:href="@{pipeline}" th:classappend="${currentPage}=='pipeline' ? 'active' : ''" th:title="#{home.pipeline.desc}">
<img class="icon" src="images/pipeline.svg" alt="icon">
<span class="icon-text" th:text="#{home.pipeline.title}"></span>
</a>
</li>-->
<li class="nav-item nav-item-separator"></li> <li class="nav-item nav-item-separator"></li>
<li class="nav-item dropdown" th:classappend="${currentPage}=='remove-pages' OR ${currentPage}=='merge-pdfs' OR ${currentPage}=='split-pdfs' OR ${currentPage}=='pdf-organizer' OR ${currentPage}=='rotate-pdf' ? 'active' : ''"> <li class="nav-item dropdown" th:classappend="${currentPage}=='remove-pages' OR ${currentPage}=='merge-pdfs' OR ${currentPage}=='split-pdfs' OR ${currentPage}=='pdf-organizer' OR ${currentPage}=='rotate-pdf' ? 'active' : ''">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">

@ -22,6 +22,7 @@
<!-- Features --> <!-- Features -->
<div class="features-container container"> <div class="features-container container">
<!-- <div th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', svgPath='images/pipeline.svg')}"></div>-->
<div th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', svgPath='images/tools.svg')}"></div> <div th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', svgPath='images/tools.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='merge-pdfs', cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', svgPath='images/union.svg')}"></div> <div th:replace="~{fragments/card :: card(id='merge-pdfs', cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', svgPath='images/union.svg')}"></div>

@ -29,7 +29,7 @@
const processFile = async (file) => { const processFile = async (file) => {
const origFileUrl = URL.createObjectURL(file); const origFileUrl = URL.createObjectURL(file);
const formPdfBytes = await fetch(origFileUrl).then(res => res.arrayBuffer()); const formPdfBytes = await fetch(origFileUrl).then(res => res.arrayBuffer());
const pdfDoc = await PDFDocument.load(formPdfBytes); const pdfDoc = await PDFDocument.load(formPdfBytes, { ignoreEncryption: true });
const form = pdfDoc.getForm(); const form = pdfDoc.getForm();
form.flatten(); form.flatten();

@ -0,0 +1,111 @@
<!DOCTYPE html>
<html th:lang="${#locale.toString()}"
th:lang-direction="#{language.direction}"
xmlns:th="http://www.thymeleaf.org">
<th:block th:insert="~{fragments/common :: head(title=#{pipeline.title})}"></th:block>
<body>
<div id="page-container">
<div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br>
<div class="container" id="dropContainer">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="mb-3">
<button id="savePipelineBtn" class="btn btn-success">Download</button>
<button id="validateButton" class="btn btn-success">Validate</button>
<div class="btn-group">
<button id="uploadPipelineBtn" class="btn btn-primary">Upload</button>
<input type="file" id="uploadPipelineInput" accept=".json"
style="display: none;">
</div>
</div>
<div id="pipelineContainer" class="card">
<!-- Pipeline Configuration Card Header -->
<div class="card-header">
<h2 class="card-title">Pipeline Configuration</h2>
</div>
<!-- Pipeline Configuration Body -->
<div class="card-body">
<div class="mb-3">
<select id="operationsDropdown" class="form-select">
<!-- Options will be dynamically populated here -->
</select>
</div>
<div class="mb-3">
<button id="addOperationBtn" class="btn btn-primary">Add operation</button>
</div>
<h3>Pipeline:</h3>
<ol id="pipelineList" class="list-group">
<!-- Pipeline operations will be dynamically populated here -->
</ol>
</div>
<input type="file" id="fileInput" multiple>
<button class="btn btn-primary" id="submitConfigBtn">Submit</button>
</div>
<!-- pipelineSettings modal -->
<div id="pipelineSettingsModal" class="modal">
<div class="modal-content">
<div class="modal-body">
<span class="close">&times;</span>
<h2>Operation Settings</h2>
<div id="pipelineSettingsContent">
<!-- pipelineSettings will be dynamically populated here -->
</div>
</div>
</div>
<script src="js/pipeline.js"></script>
</div>
</div>
</div>
</div>
</div>
<style>
.modal {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 1; /* Sit on top */
padding-top: 100px; /* Location of the box */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0, 0, 0); /* Fallback color */
background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity */
}
/* Modal Content */
.modal-content {
background-color: #fefefe;
margin: auto;
padding: 20px;
border: 1px solid #888;
width: 50%;
}
.btn-margin {
margin-right: 2px;
}
.modal-body {
display: flex;
flex-direction: column;
}
</style>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>

@ -18,6 +18,17 @@
<label th:text="#{watermark.selectText.1}"></label> <label th:text="#{watermark.selectText.1}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
</div> </div>
<div class="form-group">
<label for="fontSize" th:text="#{alphabet} + ':'"></label>
<select class="form-control" name="alphabet" id="alphabet-select">
<option value="romain">Roman</option>
<option value="arabic">العربية</option>
<option value="japanese">日本語</option>
<option value="korean">한국어</option>
<option value="chinese">简体中文</option>
</select>
</div>
<div class="form-group"> <div class="form-group">
<label for="watermarkText" th:text="#{watermark.selectText.2}"></label> <label for="watermarkText" th:text="#{watermark.selectText.2}"></label>
<input type="text" id="watermarkText" name="watermarkText" class="form-control" placeholder="Stirling-PDF" required /> <input type="text" id="watermarkText" name="watermarkText" class="form-control" placeholder="Stirling-PDF" required />