Merge pull request #256 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

View file

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

View file

@ -119,7 +119,7 @@ services:
Please view https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md
## Want to add your own language?
Stirling PDF currently supports
Stirling PDF currently supports 16!
- English (English) (en_GB)
- Arabic (العربية) (ar_AR)
- German (Deutsch) (de_DE)
@ -132,6 +132,10 @@ Stirling PDF currently supports
- Polish (Polski) (pl_PL)
- Romanian (Română) (ro_RO)
- 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
https://github.com/Frooodle/Stirling-PDF/blob/main/HowToAddNewLanguage.md

View file

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

View file

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

View file

@ -27,7 +27,8 @@ public class MetricsFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String uri = request.getRequestURI();
//System.out.println("uri="+uri + ", method=" + request.getMethod() );
// Ignore static resources
if (!(uri.startsWith("/js") || uri.startsWith("/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")
@ -36,6 +37,7 @@ public class MetricsFilter extends OncePerRequestFilter {
.register(meterRegistry);
counter.increment();
//System.out.println("Counted");
}
filterChain.doFilter(request, response);

View file

@ -17,9 +17,11 @@ 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.WebResponseUtils;
@RestController
@Tag(name = "General", description = "General APIs")
public class MergeController {
private static final Logger logger = LoggerFactory.getLogger(MergeController.class);
@ -47,7 +49,7 @@ public class MergeController {
@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
@Operation(
summary = "Merge multiple PDF files into one",
description = "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided."
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(
@RequestPart(required = true, value = "fileInput")

View file

@ -24,15 +24,17 @@ import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "General", description = "General APIs")
public class MultiPageLayoutController {
private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class);
@PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data")
@Operation(summary = "Merge multiple pages of a PDF document into a single page", description = "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file.")
@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(
@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 = {

View file

@ -18,16 +18,18 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "General", description = "General APIs")
public class RearrangePagesPDFController {
private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class);
@PostMapping(consumes = "multipart/form-data", value = "/remove-pages")
@Operation(summary = "Remove pages from a PDF file", description = "This endpoint removes specified pages from a given PDF file. Users can provide a comma-separated list of page numbers or ranges to delete.")
@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(
@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)
@ -151,7 +153,7 @@ public class RearrangePagesPDFController {
}
@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(
@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,

View file

@ -16,9 +16,11 @@ 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.WebResponseUtils;
@RestController
@Tag(name = "General", description = "General APIs")
public class RotationController {
private static final Logger logger = LoggerFactory.getLogger(RotationController.class);
@ -26,7 +28,7 @@ public class RotationController {
@PostMapping(consumes = "multipart/form-data", value = "/rotate-pdf")
@Operation(
summary = "Rotate a PDF file",
description = "This endpoint rotates a given PDF file by a specified angle. The angle must be a multiple of 90."
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(
@RequestPart(required = true, value = "fileInput")

View file

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

View file

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

View file

@ -20,16 +20,18 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertImgPDFController {
private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class);
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-img")
@Operation(summary = "Convert PDF to image(s)",
description = "This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images.")
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(
@RequestPart(required = true, value = "fileInput")
@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")
@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(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input images to be converted to a PDF file")

View file

@ -17,10 +17,12 @@ 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.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertOfficeController {
public byte[] convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
@ -57,8 +59,8 @@ public class ConvertOfficeController {
@PostMapping(consumes = "multipart/form-data", value = "/file-to-pdf")
@Operation(
summary = "Convert a file to a PDF using OCR",
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."
summary = "Convert a file to a PDF using LibreOffice",
description = "This endpoint converts a given file to a PDF using LibreOffice API Input:Any Output:PDF Type:SISO"
)
public ResponseEntity<byte[]> processPdfWithOCR(
@RequestPart(required = true, value = "fileInput")

View file

@ -12,13 +12,15 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.PDFToFile;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertPDFToOffice {
@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(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to HTML format", required = true) MultipartFile inputFile)
throws IOException, InterruptedException {
@ -27,7 +29,7 @@ public class ConvertPDFToOffice {
}
@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(
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile,
@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")
@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(
@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 = {
@ -49,7 +51,7 @@ public class ConvertPDFToOffice {
}
@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(
@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 = {
@ -60,7 +62,7 @@ public class ConvertPDFToOffice {
}
@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(
@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 {

View file

@ -14,16 +14,18 @@ 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.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertPDFToPDFA {
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-pdfa")
@Operation(
summary = "Convert a PDF to a PDF/A",
description = "This endpoint converts a PDF file to a PDF/A file. PDF/A is a format designed for long-term archiving of digital documents."
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(
@RequestPart(required = true, value = "fileInput")

View file

@ -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);
}
}
}

View file

@ -28,17 +28,19 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
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.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
public class BlankPageController {
@PostMapping(consumes = "multipart/form-data", value = "/remove-blanks")
@Operation(
summary = "Remove blank pages from a PDF file",
description = "This endpoint removes blank pages from a given PDF file. Users can specify the threshold and white percentage to tune the detection of blank pages."
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(
@RequestPart(required = true, value = "fileInput")
@ -71,7 +73,7 @@ public class BlankPageController {
pagesToKeepIndex.add(pageIndex);
System.out.println("page " + pageIndex + " has text");
} else {
boolean hasImages = hasImagesOnPage(page);
boolean hasImages = PdfUtils.hasImagesOnPage(page);
if (hasImages) {
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();
}
}

View file

@ -31,17 +31,19 @@ import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
public class CompressController {
private static final Logger logger = LoggerFactory.getLogger(CompressController.class);
@PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
@Operation(summary = "Optimize PDF file", description = "This endpoint accepts a PDF file and optimizes it based on the provided parameters.")
@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(
@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 = {

View file

@ -31,17 +31,19 @@ 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.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
public class ExtractImageScansController {
private static final Logger logger = LoggerFactory.getLogger(ExtractImageScansController.class);
@PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans")
@Operation(summary = "Extract image scans from an input file",
description = "This endpoint extracts image scans from a given file based on certain parameters. Users can specify angle threshold, tolerance, minimum area, minimum contour area, and border size.")
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(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input file containing image scans")

View file

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

View file

@ -1,12 +1,31 @@
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.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
//Other required classes
import java.util.Random;
//Required for image input/output
import javax.imageio.ImageIO;
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.LoggerFactory;
import org.springframework.http.ResponseEntity;
@ -17,46 +36,17 @@ import org.springframework.web.multipart.MultipartFile;
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.Parameter;
import stirling.software.SPDF.utils.ProcessExecutor;
import io.swagger.v3.oas.annotations.tags.Tag;
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
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
@Hidden

View file

@ -19,9 +19,11 @@ 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.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
public class MetadataController {
@ -38,7 +40,7 @@ public class MetadataController {
@PostMapping(consumes = "multipart/form-data", value = "/update-metadata")
@Operation(summary = "Update metadata of a PDF file",
description = "This endpoint allows you to update the metadata of a given PDF file. You can add, modify, or delete standard and custom metadata fields.")
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(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to update metadata")
@ -73,6 +75,7 @@ public class MetadataController {
@RequestParam(value = "trapped", required = false)
@Parameter(description = "The trapped status of the document")
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)
throws IOException {

View file

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

View file

@ -14,10 +14,12 @@ 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.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
public class OverlayImageController {
private static final Logger logger = LoggerFactory.getLogger(OverlayImageController.class);
@ -25,7 +27,7 @@ public class OverlayImageController {
@PostMapping(consumes = "multipart/form-data", value = "/add-image")
@Operation(
summary = "Overlay image onto a PDF file",
description = "This endpoint overlays an image onto a PDF file at the specified coordinates. The image can be overlaid on every page of the PDF if specified."
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(
@RequestPart(required = true, value = "fileInput")

View file

@ -16,10 +16,12 @@ 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.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Other", description = "Other APIs")
public class RepairController {
private static final Logger logger = LoggerFactory.getLogger(RepairController.class);
@ -27,7 +29,7 @@ public class RepairController {
@PostMapping(consumes = "multipart/form-data", value = "/repair")
@Operation(
summary = "Repair a PDF file",
description = "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response."
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(
@RequestPart(required = true, value = "fileInput")

View file

@ -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;
}
}

View file

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

View file

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

View file

@ -1,15 +1,23 @@
package stirling.software.SPDF.controller.api.security;
import java.awt.Color;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
import org.apache.pdfbox.util.Matrix;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
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.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.utils.WebResponseUtils;
import io.swagger.v3.oas.annotations.media.Schema;
@RestController
@Tag(name = "Security", description = "Security APIs")
public class WatermarkController {
@PostMapping(consumes = "multipart/form-data", value = "/add-watermark")
@Operation(summary = "Add watermark to a PDF file",
description = "This endpoint adds a watermark to a given PDF file. Users can specify the watermark text, font size, rotation, opacity, width spacer, and height spacer.")
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(
@RequestPart(required = true, value = "fileInput")
@Parameter(description = "The input PDF file to add a watermark")
MultipartFile pdfFile,
@RequestParam(defaultValue = "roman", name = "alphabet")
@Parameter(description = "The selected alphabet",
schema = @Schema(type = "string",
allowableValues = {"roman","arabic","japanese","korean","chinese"},
defaultValue = "roman"))
String alphabet,
@RequestParam("watermarkText")
@Parameter(description = "The watermark text to add to the PDF file")
String watermarkText,
@ -48,11 +64,11 @@ public class WatermarkController {
int widthSpacer,
@RequestParam(defaultValue = "50", name = "heightSpacer")
@Parameter(description = "The height spacer between watermark texts", example = "50")
int heightSpacer) throws IOException {
int heightSpacer) throws IOException, Exception {
// Load the input PDF
PDDocument document = PDDocument.load(pdfFile.getInputStream());
String producer = document.getDocumentInformation().getProducer();
// Create a page in the document
for (PDPage page : document.getPages()) {
@ -64,8 +80,40 @@ public class WatermarkController {
graphicsState.setNonStrokingAlphaConstant(opacity);
contentStream.setGraphicsStateParameters(graphicsState);
// Set font of watermark
PDFont font = PDType1Font.HELVETICA_BOLD;
String resourceDir = "";
PDFont font = PDType1Font.HELVETICA_BOLD;
switch (alphabet) {
case "arabic":
resourceDir = "static/fonts/NotoSansArabic-Regular.ttf";
break;
case "japanese":
resourceDir = "static/fonts/Meiryo.ttf";
break;
case "korean":
resourceDir = "static/fonts/malgun.ttf";
break;
case "chinese":
resourceDir = "static/fonts/SimSun.ttf";
break;
case "roman":
default:
resourceDir = "static/fonts/NotoSans-Regular.ttf";
break;
}
if(!resourceDir.equals("")) {
ClassPathResource classPathResource = new ClassPathResource(resourceDir);
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
File tempFile = File.createTempFile("NotoSansFont", fileExtension);
try (InputStream is = classPathResource.getInputStream(); FileOutputStream os = new FileOutputStream(tempFile)) {
IOUtils.copy(is, os);
}
font = PDType0Font.load(document, tempFile);
tempFile.deleteOnExit();
}
contentStream.beginText();
contentStream.setFont(font, fontSize);
contentStream.setNonStrokingColor(Color.LIGHT_GRAY);
@ -81,11 +129,19 @@ public class WatermarkController {
// Add the watermark text
for (int i = 0; i < watermarkRows; i++) {
for (int j = 0; j < watermarkCols; j++) {
contentStream.setTextMatrix(Matrix.getRotateInstance((float) Math.toRadians(rotation), j * watermarkWidth, i * watermarkHeight));
if(producer.contains("Google Docs")) {
//This fixes weird unknown google docs y axis rotation/flip issue
//TODO: Long term fix one day
//contentStream.setTextMatrix(1, 0, 0, -1, j * watermarkWidth, pageHeight - i * watermarkHeight);
Matrix matrix = new Matrix(1, 0, 0, -1, j * watermarkWidth, pageHeight - i * watermarkHeight);
contentStream.setTextMatrix(matrix);
} else {
contentStream.setTextMatrix(Matrix.getRotateInstance((float) Math.toRadians(rotation), j * watermarkWidth, i * watermarkHeight));
}
contentStream.showTextWithPositioning(new Object[] { watermarkText });
}
}
contentStream.endText();
// Close the content stream

View file

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

View file

@ -1,27 +1,29 @@
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;
import io.swagger.v3.oas.annotations.tags.Tag;
@Controller
@Tag(name = "General", description = "General APIs")
public class GeneralWebController {
@GetMapping("/pipeline")
@Hidden
public String pipelineForm(Model model) {
model.addAttribute("currentPage", "pipeline");
return "pipeline";
}
@GetMapping("/merge-pdfs")
@Hidden
public String mergePdfForm(Model model) {
model.addAttribute("currentPage", "merge-pdfs");
return "merge-pdfs";
}
@GetMapping("/about")
@Hidden
public String gameForm(Model model) {
model.addAttribute("currentPage", "about");
return "about";
}
@GetMapping("/multi-tool")
@Hidden
@ -29,17 +31,7 @@ public class GeneralWebController {
model.addAttribute("currentPage", "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")
@Hidden
@ -76,20 +68,4 @@ public class GeneralWebController {
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: /";
}
}
}

View file

@ -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: /";
}
}
}

View file

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

View file

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

View file

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

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -27,14 +27,179 @@ 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.apache.pdfbox.text.PDFTextStripper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 {
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 {
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(inputStream))) {
PDFRenderer pdfRenderer = new PDFRenderer(document);
@ -43,7 +208,7 @@ public class PdfUtils {
// Create images of all pages
for (int i = 0; i < pageCount; i++) {
images.add(pdfRenderer.renderImageWithDPI(i, 300, colorType));
images.add(pdfRenderer.renderImageWithDPI(i, DPI, colorType));
}
if (singleImage) {

View file

@ -1,12 +1,12 @@
spring.http.multipart.max-file-size=2GB
spring.http.multipart.max-request-size=2GB
spring.http.multipart.max-file-size=${MAX_FILE_SIZE:2000MB}
spring.http.multipart.max-request-size=${MAX_FILE_SIZE:2000MB}
multipart.enabled=true
multipart.max-file-size=2000MB
multipart.max-request-size=2000MB
multipart.max-file-size=${MAX_FILE_SIZE:2000MB}
multipart.max-request-size=${MAX_FILE_SIZE:2000MB}
spring.servlet.multipart.max-file-size=2000MB
spring.servlet.multipart.max-request-size=2000MB
spring.servlet.multipart.max-file-size=${MAX_FILE_SIZE:2000MB}
spring.servlet.multipart.max-request-size=${MAX_FILE_SIZE:2000MB}
server.forward-headers-strategy=NATIVE
@ -22,7 +22,7 @@ server.servlet.context-path=${APP_ROOT_PATH:/}
spring.devtools.restart.enabled=true
spring.devtools.livereload.enabled=true
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.encoding=UTF-8
server.connection-timeout=${CONNECTION_TIMEOUT:5m}
spring.mvc.async.request-timeout=${ASYNC_CONNECTION_TIMEOUT:300000}

View file

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

View file

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

View file

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

View file

@ -20,6 +20,7 @@ close=Close
filesSelected=files selected
noFavourites=No favourites added
bored=Bored Waiting?
alphabet=Alphabet
#############
# 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.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
downloadPdf=Download PDF
@ -139,6 +143,8 @@ font=Font
selectFillter=-- Select --
pageNum=Page Number
pipeline.title=Pipeline
pageLayout.title=Multi Page Layout
pageLayout.header=Multi Page Layout
pageLayout.pagesPerSheet=Pages per sheet:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Width:  |  Height:  |  Size: 790 B

View file

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

View file

@ -152,7 +152,7 @@ class PdfContainer {
async toPdfLib(objectUrl) {
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;
}

View file

@ -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]);
});
});

View file

@ -32,6 +32,13 @@
<span class="icon-text" th:text="#{home.multiTool.title}"></span>
</a>
</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 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">

View file

@ -22,6 +22,7 @@
<!-- Features -->
<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='merge-pdfs', cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', svgPath='images/union.svg')}"></div>

View file

@ -29,7 +29,7 @@
const processFile = async (file) => {
const origFileUrl = URL.createObjectURL(file);
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();
form.flatten();

View file

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

View file

@ -18,6 +18,17 @@
<label th:text="#{watermark.selectText.1}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></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">
<label for="watermarkText" th:text="#{watermark.selectText.2}"></label>
<input type="text" id="watermarkText" name="watermarkText" class="form-control" placeholder="Stirling-PDF" required />