diff --git a/Dockerfile b/Dockerfile index 5f540419..b1d91a48 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md index 39f0367a..411e613b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java index de43b72c..402c98fc 100644 --- a/src/main/java/stirling/software/SPDF/SPdfApplication.java +++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java @@ -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 diff --git a/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java b/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java index 81cbc43f..efa084b3 100644 --- a/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java +++ b/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java @@ -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 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 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 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) { + } } diff --git a/src/main/java/stirling/software/SPDF/config/MetricsFilter.java b/src/main/java/stirling/software/SPDF/config/MetricsFilter.java index d4f64596..87edc0f6 100644 --- a/src/main/java/stirling/software/SPDF/config/MetricsFilter.java +++ b/src/main/java/stirling/software/SPDF/config/MetricsFilter.java @@ -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); diff --git a/src/main/java/stirling/software/SPDF/controller/api/MergeController.java b/src/main/java/stirling/software/SPDF/controller/api/MergeController.java index 3f3ab8c1..7639d154 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/MergeController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/MergeController.java @@ -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 mergePdfs( @RequestPart(required = true, value = "fileInput") diff --git a/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java b/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java index 7dc9c51f..9bf0a3d7 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java @@ -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 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 = { diff --git a/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java b/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java index c1a0c892..d9cb2686 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java @@ -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 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 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, diff --git a/src/main/java/stirling/software/SPDF/controller/api/RotationController.java b/src/main/java/stirling/software/SPDF/controller/api/RotationController.java index 33bb793b..916d2afb 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/RotationController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/RotationController.java @@ -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 rotatePDF( @RequestPart(required = true, value = "fileInput") diff --git a/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java b/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java index fc743d0c..420902cf 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java @@ -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 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 = { diff --git a/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java b/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java index afee784c..abc201ab 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java @@ -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 splitPdf( @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be split") diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java index 84b5a7b1..d19a24b6 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java @@ -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 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 convertToPdf( @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input images to be converted to a PDF file") diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java index 7b806041..79be9e2e 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java @@ -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 processPdfWithOCR( @RequestPart(required = true, value = "fileInput") diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java index 6ac5b22d..7c99ee4d 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java @@ -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 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 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 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 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 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 { diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java index ab269fa3..4ff2b4f2 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java @@ -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 pdfToPdfA( @RequestPart(required = true, value = "fileInput") diff --git a/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java b/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java new file mode 100644 index 00000000..13732ba3 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java @@ -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); + } + } + +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/BlankPageController.java b/src/main/java/stirling/software/SPDF/controller/api/other/BlankPageController.java index 0e17c0ba..6ed76edb 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/BlankPageController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/BlankPageController.java @@ -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 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(); - } + } diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/CompressController.java b/src/main/java/stirling/software/SPDF/controller/api/other/CompressController.java index d8917bea..0002b808 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/CompressController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/CompressController.java @@ -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 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 = { diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/ExtractImageScansController.java b/src/main/java/stirling/software/SPDF/controller/api/other/ExtractImageScansController.java index b97ed8e2..f9ac6761 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/ExtractImageScansController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/ExtractImageScansController.java @@ -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 extractImageScans( @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input file containing image scans") diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/ExtractImagesController.java b/src/main/java/stirling/software/SPDF/controller/api/other/ExtractImagesController.java index 66f32539..c10dd7b4 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/ExtractImagesController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/ExtractImagesController.java @@ -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 extractImages( @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file containing images") diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/FakeScanController.java b/src/main/java/stirling/software/SPDF/controller/api/other/FakeScanControllerWIP.java similarity index 95% rename from src/main/java/stirling/software/SPDF/controller/api/other/FakeScanController.java rename to src/main/java/stirling/software/SPDF/controller/api/other/FakeScanControllerWIP.java index a4876ad0..7a25d28c 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/FakeScanController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/FakeScanControllerWIP.java @@ -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 diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/MetadataController.java b/src/main/java/stirling/software/SPDF/controller/api/other/MetadataController.java index 7eed7c5f..e3979d10 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/MetadataController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/MetadataController.java @@ -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 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 allRequestParams) throws IOException { diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/OCRController.java b/src/main/java/stirling/software/SPDF/controller/api/other/OCRController.java index 5aa00650..c3c323f5 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/OCRController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/OCRController.java @@ -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 processPdfWithOCR( @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be processed with OCR") diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/OverlayImageController.java b/src/main/java/stirling/software/SPDF/controller/api/other/OverlayImageController.java index 2afc42c5..61768f6a 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/OverlayImageController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/OverlayImageController.java @@ -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 overlayImage( @RequestPart(required = true, value = "fileInput") diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/RepairController.java b/src/main/java/stirling/software/SPDF/controller/api/other/RepairController.java index 58eeabeb..536f8c89 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/RepairController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/RepairController.java @@ -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 repairPdf( @RequestPart(required = true, value = "fileInput") diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/Controller.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/Controller.java new file mode 100644 index 00000000..d0325450 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/Controller.java @@ -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 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 paths = Files.list(dir)) { + files = paths.filter(path -> !path.equals(jsonFile)) + .map(Path::toFile) + .toArray(File[]::new); + } catch (IOException e) { + e.printStackTrace(); + return; + } + } else { + // If fileInput contains a path, process only this file + files = new File[]{new File(fileInput)}; + } + + // Prepare the files for processing + File[] filesToProcess = files.clone(); + for (File file : filesToProcess) { + Files.move(file.toPath(), processingDir.resolve(file.getName())); + } + + // Process the files + try { + List resources = handleFiles(filesToProcess, jsonString); + + // Move resultant files and rename them as per config in JSON file + for (Resource resource : resources) { + String outputFileName = config.getOutputPattern().replace("{filename}", resource.getFile().getName()); + outputFileName = outputFileName.replace("{pipelineName}", config.getName()); + DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd"); + outputFileName = outputFileName.replace("{date}", LocalDate.now().format(dateFormatter)); + DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HHmmss"); + outputFileName = outputFileName.replace("{time}", LocalTime.now().format(timeFormatter)); + // {filename} {folder} {date} {tmime} {pipeline} + + Files.move(resource.getFile().toPath(), Paths.get(config.getOutputDir(), outputFileName)); + } + + // If successful, delete the original files + for (File file : filesToProcess) { + Files.deleteIfExists(processingDir.resolve(file.getName())); + } + } catch (Exception e) { + // If an error occurs, move the original files back + for (File file : filesToProcess) { + Files.move(processingDir.resolve(file.getName()), file.toPath()); + } + throw e; + } + } + } + } + + + + +List processFiles(List outputFiles, String jsonString) throws Exception{ + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonNode = mapper.readTree(jsonString); + + JsonNode pipelineNode = jsonNode.get("pipeline"); + ByteArrayOutputStream logStream = new ByteArrayOutputStream(); + PrintStream logPrintStream = new PrintStream(logStream); + + boolean hasErrors = false; + + for (JsonNode operationNode : pipelineNode) { + String operation = operationNode.get("operation").asText(); + JsonNode parametersNode = operationNode.get("parameters"); + String inputFileExtension = ""; + if(operationNode.has("inputFileType")) { + inputFileExtension = operationNode.get("inputFileType").asText(); + } else { + inputFileExtension=".pdf"; + } + + List newOutputFiles = new ArrayList<>(); + boolean hasInputFileType = false; + + for (Resource file : outputFiles) { + if (file.getFilename().endsWith(inputFileExtension)) { + hasInputFileType = true; + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("fileInput", file); + + Iterator> parameters = parametersNode.fields(); + while (parameters.hasNext()) { + Map.Entry parameter = parameters.next(); + body.add(parameter.getKey(), parameter.getValue().asText()); + } + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + HttpEntity> entity = new HttpEntity<>(body, headers); + + RestTemplate restTemplate = new RestTemplate(); + String url = "http://localhost:8080/" + operation; + + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class); + + if (!response.getStatusCode().equals(HttpStatus.OK)) { + logPrintStream.println("Error: " + response.getBody()); + hasErrors = true; + continue; + } + + // Check if the response body is a zip file + if (isZip(response.getBody())) { + // Unzip the file and add all the files to the new output files + newOutputFiles.addAll(unzip(response.getBody())); + } else { + Resource outputResource = new ByteArrayResource(response.getBody()) { + @Override + public String getFilename() { + return file.getFilename(); // Preserving original filename + } + }; + newOutputFiles.add(outputResource); + } + } + + if (!hasInputFileType) { + logPrintStream.println("No files with extension " + inputFileExtension + " found for operation " + operation); + hasErrors = true; + } + + outputFiles = newOutputFiles; + } + logPrintStream.close(); + + } + return outputFiles; +} + + +List handleFiles(File[] files, String jsonString) throws Exception{ + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonNode = mapper.readTree(jsonString); + + JsonNode pipelineNode = jsonNode.get("pipeline"); + ByteArrayOutputStream logStream = new ByteArrayOutputStream(); + PrintStream logPrintStream = new PrintStream(logStream); + + boolean hasErrors = false; + List outputFiles = new ArrayList<>(); + + for (File file : files) { + Path path = Paths.get(file.getAbsolutePath()); + Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) { + @Override + public String getFilename() { + return file.getName(); + } + }; + outputFiles.add(fileResource); + } + return processFiles(outputFiles, jsonString); +} + + List handleFiles(MultipartFile[] files, String jsonString) throws Exception{ + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonNode = mapper.readTree(jsonString); + + JsonNode pipelineNode = jsonNode.get("pipeline"); + ByteArrayOutputStream logStream = new ByteArrayOutputStream(); + PrintStream logPrintStream = new PrintStream(logStream); + + boolean hasErrors = false; + List outputFiles = new ArrayList<>(); + + for (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 handleData(@RequestPart("fileInput") MultipartFile[] files, + @RequestParam("json") String jsonString) { + try { + + List outputFiles = handleFiles(files, jsonString); + + if (outputFiles.size() == 1) { + // If there is only one file, return it directly + Resource singleFile = outputFiles.get(0); + InputStream is = singleFile.getInputStream(); + byte[] bytes = new byte[(int)singleFile.contentLength()]; + is.read(bytes); + is.close(); + + return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM); + } + + // 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 unzip(byte[] data) throws IOException { + List 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; + } +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java index 9f9dddd8..55000dc7 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java @@ -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 signPDF( @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be signed") diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java b/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java index 97264b9a..fa6df6ef 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java @@ -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 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 addPassword( @RequestPart(required = true, value = "fileInput") diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java b/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java index 03be0160..4ef8604b 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java @@ -1,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 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 diff --git a/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java b/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java index f110cdc8..e8c1fa76 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java @@ -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") diff --git a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java index e3df1fb0..11a7c9bf 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java @@ -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: /"; - } - } - } diff --git a/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java b/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java new file mode 100644 index 00000000..ba857afd --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java @@ -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: /"; + } + } + +} diff --git a/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java b/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java index de73f9e2..70235df3 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java @@ -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 endpoint) { + public Double getPageLoads(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional 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 endpoint) { + public Double getTotalRequests(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional 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; } } diff --git a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java index 90a18de9..2c11decc 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java @@ -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 diff --git a/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java b/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java index 98821c85..0dbb226b 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java @@ -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 diff --git a/src/main/java/stirling/software/SPDF/model/PipelineConfig.java b/src/main/java/stirling/software/SPDF/model/PipelineConfig.java new file mode 100644 index 00000000..77ef7a05 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/model/PipelineConfig.java @@ -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 operations; + + private String outputDir; + + @JsonProperty("outputFileName") + private String outputPattern; + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getOperations() { + return operations; + } + + public void setOperations(List 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; + } + + +} diff --git a/src/main/java/stirling/software/SPDF/model/PipelineOperation.java b/src/main/java/stirling/software/SPDF/model/PipelineOperation.java new file mode 100644 index 00000000..8b079ba1 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/model/PipelineOperation.java @@ -0,0 +1,25 @@ +package stirling.software.SPDF.model; + +import java.util.Map; + +public class PipelineOperation { + private String operation; + private Map parameters; + + + public String getOperation() { + return operation; + } + + public void setOperation(String operation) { + this.operation = operation; + } + + public Map getParameters() { + return parameters; + } + + public void setParameters(Map parameters) { + this.parameters = parameters; + } + } \ No newline at end of file diff --git a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java index e5d35c08..5e9d53b0 100644 --- a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java @@ -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) { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index be16c7ec..6d5dfe95 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -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} diff --git a/src/main/resources/messages_ar_AR.properties b/src/main/resources/messages_ar_AR.properties index 1b52ba8a..f3991b4d 100644 --- a/src/main/resources/messages_ar_AR.properties +++ b/src/main/resources/messages_ar_AR.properties @@ -24,7 +24,7 @@ close=\u0625\u063A\u0644\u0627\u0642 filesSelected = الملفات المحددة noFavourites = لم تتم إضافة أي مفضلات bored = الانتظار بالملل؟ - +alphabet=\u0627\u0644\u0623\u0628\u062C\u062F\u064A\u0629 ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_ca_CA.properties b/src/main/resources/messages_ca_CA.properties index 0d785b2a..39d28ac5 100644 --- a/src/main/resources/messages_ca_CA.properties +++ b/src/main/resources/messages_ca_CA.properties @@ -20,6 +20,7 @@ close=Tanca filesSelected=fitxers seleccionats noFavourites=No s'ha afegit cap favorit bored=Avorrit esperant? +alphabet=Alfabet ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_de_DE.properties b/src/main/resources/messages_de_DE.properties index 47872c91..278f021b 100644 --- a/src/main/resources/messages_de_DE.properties +++ b/src/main/resources/messages_de_DE.properties @@ -20,6 +20,7 @@ close=Schließen filesSelected=Dateien ausgewählt noFavourites=Keine Favoriten hinzugefügt bored=Gelangweiltes Warten? +alphabet=Alphabet ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index c8f1ae13..c4d3dc2f 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -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: diff --git a/src/main/resources/messages_es_ES.properties b/src/main/resources/messages_es_ES.properties index a5958a67..43bb4c06 100644 --- a/src/main/resources/messages_es_ES.properties +++ b/src/main/resources/messages_es_ES.properties @@ -20,6 +20,7 @@ close=Cerrar filesSelected=archivos seleccionados noFavourites=No se agregaron favoritos bored=¿Aburrido de esperar? +alphabet=Alfabeto ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_eu_ES.properties b/src/main/resources/messages_eu_ES.properties index 320e9c99..58d969a7 100644 --- a/src/main/resources/messages_eu_ES.properties +++ b/src/main/resources/messages_eu_ES.properties @@ -20,6 +20,7 @@ close=Itxi filesSelected=Hautatutako fitxategiak noFavourites=Ez dira gogokoak gehitu bored=Itxaroten aspertuta? +alphabet=Alfabetoa ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_fr_FR.properties b/src/main/resources/messages_fr_FR.properties index fdc3a81a..07905cfa 100644 --- a/src/main/resources/messages_fr_FR.properties +++ b/src/main/resources/messages_fr_FR.properties @@ -24,6 +24,7 @@ close=Fermer filesSelected=fichiers sélectionnés noFavourites=Aucun favori ajouté bored=Ennuyé d'attendre ? +alphabet=Alphabet ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_it_IT.properties b/src/main/resources/messages_it_IT.properties index 7512a29b..cd6a5aa2 100644 --- a/src/main/resources/messages_it_IT.properties +++ b/src/main/resources/messages_it_IT.properties @@ -20,6 +20,7 @@ close=Chiudi filesSelected=file selezionati noFavourites=Nessun preferito bored=Stanco di aspettare? +alphabet=Alfabeto ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_ja_JP.properties b/src/main/resources/messages_ja_JP.properties index b0c96529..5aecb639 100644 --- a/src/main/resources/messages_ja_JP.properties +++ b/src/main/resources/messages_ja_JP.properties @@ -19,7 +19,8 @@ save=保存 close=閉じる filesSelected=選択されたファイル noFavourites=お気に入りはありません -bored=待ち時間が退屈? +bored=待ち時間が退屈 +alphabet=\u30A2\u30EB\u30D5\u30A1\u30D9\u30C3\u30C8 ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_ko_KR.properties b/src/main/resources/messages_ko_KR.properties index b978ab2d..fde2e5b3 100644 --- a/src/main/resources/messages_ko_KR.properties +++ b/src/main/resources/messages_ko_KR.properties @@ -20,6 +20,7 @@ close=닫기 filesSelected=개 파일 선택됨 noFavourites=즐겨찾기 없음 bored=기다리는 게 지루하신가요? +alphabet=\uC54C\uD30C\uBCB3 ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_pl_PL.properties b/src/main/resources/messages_pl_PL.properties index 19244f53..8e7434ce 100644 --- a/src/main/resources/messages_pl_PL.properties +++ b/src/main/resources/messages_pl_PL.properties @@ -20,6 +20,7 @@ close=Zamknij filesSelected=wybrane pliki noFavourites=Nie dodano ulubionych bored=Znudzony czekaniem? +alphabet=Alfabet ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_pt_BR.properties b/src/main/resources/messages_pt_BR.properties index c09b4ab3..842b93ce 100644 --- a/src/main/resources/messages_pt_BR.properties +++ b/src/main/resources/messages_pt_BR.properties @@ -20,6 +20,7 @@ close=Fechar filesSelected=arquivos selecionados noFavourites=Nenhum favorito adicionado bored=Entediado esperando? +alphabet=Alfabeto ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_ro_RO.properties b/src/main/resources/messages_ro_RO.properties index 3ea7829a..9a96fd11 100644 --- a/src/main/resources/messages_ro_RO.properties +++ b/src/main/resources/messages_ro_RO.properties @@ -20,6 +20,7 @@ close=Închide filesSelected=fișiere selectate noFavourites=Niciun favorit adăugat bored=Plictisit așteptând? +alphabet=Alfabet ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_ru_RU.properties b/src/main/resources/messages_ru_RU.properties index c7617c45..6f84ee5a 100644 --- a/src/main/resources/messages_ru_RU.properties +++ b/src/main/resources/messages_ru_RU.properties @@ -20,6 +20,7 @@ close=Закрыть filesSelected=файлов выбрано noFavourites=Нет избранного bored=Скучно ждать? +alphabet=\u0430\u043B\u0444\u0430\u0432\u0438\u0442 ############# # HOME-PAGE # ############# diff --git a/src/main/resources/messages_sv_SE.properties b/src/main/resources/messages_sv_SE.properties index 224fc41b..6608479d 100644 --- a/src/main/resources/messages_sv_SE.properties +++ b/src/main/resources/messages_sv_SE.properties @@ -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 # ############# diff --git a/src/main/resources/messages_zh_CN.properties b/src/main/resources/messages_zh_CN.properties index 6ae7df00..0449ae93 100644 --- a/src/main/resources/messages_zh_CN.properties +++ b/src/main/resources/messages_zh_CN.properties @@ -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 # ############# diff --git a/src/main/resources/static/fonts/Meiryo.ttf b/src/main/resources/static/fonts/Meiryo.ttf new file mode 100644 index 00000000..a608fbb4 Binary files /dev/null and b/src/main/resources/static/fonts/Meiryo.ttf differ diff --git a/src/main/resources/static/fonts/NotoSans-Regular.ttf b/src/main/resources/static/fonts/NotoSans-Regular.ttf new file mode 100644 index 00000000..7552fbe8 Binary files /dev/null and b/src/main/resources/static/fonts/NotoSans-Regular.ttf differ diff --git a/src/main/resources/static/fonts/NotoSansArabic-Regular.ttf b/src/main/resources/static/fonts/NotoSansArabic-Regular.ttf new file mode 100644 index 00000000..79359c46 Binary files /dev/null and b/src/main/resources/static/fonts/NotoSansArabic-Regular.ttf differ diff --git a/src/main/resources/static/fonts/NotoSansJP-Regular.ttf b/src/main/resources/static/fonts/NotoSansJP-Regular.ttf new file mode 100644 index 00000000..1583096a Binary files /dev/null and b/src/main/resources/static/fonts/NotoSansJP-Regular.ttf differ diff --git a/src/main/resources/static/fonts/NotoSansSC-Regular.ttf b/src/main/resources/static/fonts/NotoSansSC-Regular.ttf new file mode 100644 index 00000000..c10d2aa1 Binary files /dev/null and b/src/main/resources/static/fonts/NotoSansSC-Regular.ttf differ diff --git a/src/main/resources/static/fonts/SimSun.ttf b/src/main/resources/static/fonts/SimSun.ttf new file mode 100644 index 00000000..e0115abe Binary files /dev/null and b/src/main/resources/static/fonts/SimSun.ttf differ diff --git a/src/main/resources/static/fonts/malgun.ttf b/src/main/resources/static/fonts/malgun.ttf new file mode 100644 index 00000000..6d8645bc Binary files /dev/null and b/src/main/resources/static/fonts/malgun.ttf differ diff --git a/src/main/resources/static/fonts/static/NotoSansArabic-Regular.ttf b/src/main/resources/static/fonts/static/NotoSansArabic-Regular.ttf new file mode 100644 index 00000000..79359c46 Binary files /dev/null and b/src/main/resources/static/fonts/static/NotoSansArabic-Regular.ttf differ diff --git a/src/main/resources/static/fonts/static/NotoSansJP-Regular.ttf b/src/main/resources/static/fonts/static/NotoSansJP-Regular.ttf new file mode 100644 index 00000000..1583096a Binary files /dev/null and b/src/main/resources/static/fonts/static/NotoSansJP-Regular.ttf differ diff --git a/src/main/resources/static/images/pipeline.svg b/src/main/resources/static/images/pipeline.svg new file mode 100644 index 00000000..48722d0c --- /dev/null +++ b/src/main/resources/static/images/pipeline.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/main/resources/static/js/draggable-utils.js b/src/main/resources/static/js/draggable-utils.js index c1662f22..f90a80f1 100644 --- a/src/main/resources/static/js/draggable-utils.js +++ b/src/main/resources/static/js/draggable-utils.js @@ -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); diff --git a/src/main/resources/static/js/multitool/PdfContainer.js b/src/main/resources/static/js/multitool/PdfContainer.js index 5cb240d7..2ce87cb2 100644 --- a/src/main/resources/static/js/multitool/PdfContainer.js +++ b/src/main/resources/static/js/multitool/PdfContainer.js @@ -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; } diff --git a/src/main/resources/static/js/pipeline.js b/src/main/resources/static/js/pipeline.js new file mode 100644 index 00000000..d92fd220 --- /dev/null +++ b/src/main/resources/static/js/pipeline.js @@ -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 = ` +
+
${selectedOperation}
+
+ + + + +
+
+ `; + + 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]); + }); + +}); \ No newline at end of file diff --git a/src/main/resources/templates/fragments/navbar.html b/src/main/resources/templates/fragments/navbar.html index a38060ea..db6308e3 100644 --- a/src/main/resources/templates/fragments/navbar.html +++ b/src/main/resources/templates/fragments/navbar.html @@ -32,6 +32,13 @@ + +