diff --git a/.gitignore b/.gitignore index 88be02aa..3c11770c 100644 --- a/.gitignore +++ b/.gitignore @@ -109,4 +109,6 @@ local.properties *.tar.gz *.rar -/build \ No newline at end of file +/build + +/.vscode \ No newline at end of file diff --git a/HowToAddNewLanguage.md b/HowToAddNewLanguage.md index 18799b2b..26a398bf 100644 --- a/HowToAddNewLanguage.md +++ b/HowToAddNewLanguage.md @@ -8,11 +8,18 @@ Fork Stirling-PDF and make a new branch out of Main Then add reference to the language in the navbar by adding a new language entry to the dropdown -https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/templates/fragments/navbar.html#L80 +https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/templates/fragments/navbar.html#L306 +and add a flag svg file to +https://github.com/Frooodle/Stirling-PDF/tree/main/src/main/resources/static/images/flags +Any SVG flags are fine, i got most of mine from [here](https://flagicons.lipis.dev/) +If your language isnt represented by a flag just find whichever closely matches it, such as for Arabic i chose Saudi Arabia + For example to add Polish you would add ``` -Polish + + icon Polski + ``` The data-language-code is the code used to reference the file in the next step. diff --git a/HowToUseOCR.md b/HowToUseOCR.md index 9a867cbb..b015f53d 100644 --- a/HowToUseOCR.md +++ b/HowToUseOCR.md @@ -34,14 +34,14 @@ services: your_service_name: image: your_docker_image_name volumes: - - /usr/share/tesseract-ocr/4.00/tessdata:/location/of/trainingData + - /location/of/trainingData:/usr/share/tesseract-ocr/4.00/tessdata ``` #### Docker run Add the following to your existing docker run command ```bash --v /usr/share/tesseract-ocr/4.00/tessdata:/location/of/trainingData +-v /location/of/trainingData:/usr/share/tesseract-ocr/4.00/tessdata ``` #### Non-Docker diff --git a/LocalRunGuide.md b/LocalRunGuide.md new file mode 100644 index 00000000..a1fd49e6 --- /dev/null +++ b/LocalRunGuide.md @@ -0,0 +1,137 @@ + +To run the application without Docker, you will need to manually install all dependencies and build the necessary components. + +Note that some dependencies might not be available in the standard repositories of all Linux distributions, and may require additional steps to install. + +The following guide assumes you have a basic understanding of using a command line interface in your operating system. + +It should work on most Linux distributions and MacOS. For Windows, you might need to use Windows Subsystem for Linux (WSL) for certain steps. +The amount of dependencies is to actually reduce overall size, ie installing LibreOffice sub components rather than full LibreOffice package. + +### Step 1: Prerequisites + +Install the following software, if not already installed: + +- Java 17 or later + +- Gradle 7.0 or later (included within repo so not needed on server) + +- Git + +- Python 3 (with pip) + +- Make + +- GCC/G++ + +- Automake + +- Autoconf + +- libtool + +- pkg-config + +- zlib1g-dev + +- libleptonica-dev + +For Debian-based systems, you can use the following command: + +```bash +sudo apt-get update +sudo apt-get install -y git automake autoconf libtool libleptonica-dev pkg-config zlib1g-dev make g++ java-17-openjdk python3 python3-pip +``` + +### Step 2: Clone and Build jbig2enc (Only required for certain OCR functionality) + +```bash +git clone https:github.com/agl/jbig2enc +cd jbig2enc +./autogen.sh +./configure +make +sudo make install +``` + +### Step 3: Install Additional Software +Next we need to install LibreOffice for conversions, ocrmypdf for OCR, and opencv for patern recognition functionality. + +Install the following software: + +- libreoffice-core + +- libreoffice-common + +- libreoffice-writer + +- libreoffice-calc + +- libreoffice-impress + +- python3-uno + +- unoconv + +- pngquant + +- unpaper + +- ocrmypdf + +- opencv-python-headless + +For Debian-based systems, you can use the following command: + +```bash +sudo apt-get install -y libreoffice-core libreoffice-common libreoffice-writer libreoffice-calc libreoffice-impress python3-uno unoconv pngquant unpaper ocrmypdf +pip3 install opencv-python-headless +``` + +### Step 4: Clone and Build Stirling-PDF + +```bash +git clone https://github.com/Frooodle/Stirling-PDF.git +cd Stirling-PDF +./gradlew build +``` + + +### Step 5: Move jar to desired location + +After the build process, a `.jar` file will be generated in the `build/libs` directory. +You can move this file to a desired location, for example, `/opt/Stirling-PDF/`. +You must also move the Script folder within the Stirling-PDF repo that you have downloaded to this directory. +This folder is required for the python scripts using OpenCV + +### Step 6: Other files +#### OCR +If you plan to use the OCR (Optical Character Recognition) functionality, you might need to install language packs for Tesseract if running none english scanning. + +##### Installing Language Packs + +1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need. +2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tesseract-ocr/4.00/tessdata` +Please view [OCRmyPDF install guide](https:ocrmypdf.readthedocs.io/en/latest/installation.html) for more info. +**IMPORTANT:** DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED. + + + +### Step 7: Run Stirling-PDF + +```bash +./gradlew bootRun +or +java -jar build/libs/app.jar +``` + +Remember to set the necessary environment variables before running the project if you want to customize the application the list can be seen in the main readme. + +You can do this in the terminal by using the `export` command or -D arguements to java -jar command: + +```bash +export APP_HOME_NAME="Stirling PDF" +or +-DAPP_HOME_NAME="Stirling PDF" +``` + diff --git a/README.md b/README.md index b5264c07..799b7506 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,12 @@ Feel free to request any features or bug fixes either in github issues or our [D - Merge multiple PDFs together into a single resultant file - Convert PDFs to and from images - Reorganize PDF pages into different orders. -- Add images to PDFs at specified locations. (WIP) +- Add/Generate signatures +- Flatten PDFs +- Repair PDFs +- Detect and remove blank pages +- Compare 2 PDFs and show differences in text +- Add images to PDFs - Rotating PDFs in 90 degree increments. - Compressing PDFs to decrease their filesize. (Using OCRMyPDF) - Add and remove passwords @@ -35,6 +40,9 @@ Feel free to request any features or bug fixes either in github issues or our [D - Dark mode support. - Custom download options (see [here](https://github.com/Frooodle/Stirling-PDF/blob/main/images/settings.png) for example) - Parallel file processing and downloads +- API for integration with external scripts + +Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) hosted by the team at adminforge.de ## Technologies used - Spring Boot + Thymeleaf @@ -49,31 +57,47 @@ Feel free to request any features or bug fixes either in github issues or our [D ## How to use ### Locally - -Prerequisites -- Java 17 or later -- Gradle 7.0 or later - -1. Clone or download the repository. -2. Build the project using Gradle by running `./gradlew build` -3. Start the application by running `./gradlew bootRun` or by calling the build jar in build/libs with java -jar jarName.jar - +Please view https://github.com/Frooodle/Stirling-PDF/blob/main/LocalRunGuide.md ### Docker https://hub.docker.com/r/frooodle/s-pdf Docker Run ``` -docker run -p 8080:8080 frooodle/s-pdf +docker run -d \ + -p 8080:8080 \ + -v /location/of/trainingData:/usr/share/tesseract-ocr/4.00/tessdata \ + --name stirling-pdf \ + frooodle/s-pdf + + + Can also add these for customisation but are not required + -e APP_HOME_NAME="Stirling PDF" \ + -e APP_HOME_DESCRIPTION="Your locally hosted one-stop-shop for all your PDF needs." \ + -e APP_NAVBAR_NAME="Stirling PDF" \ + -e ALLOW_GOOGLE_VISABILITY="true" \ + -e APP_ROOT_PATH="/" \ + -e APP_LOCALE="en_GB" \ ``` Docker Compose ``` version: '3.3' services: - s-pdf: - ports: - - '8080:8080' - image: frooodle/s-pdf + stirling-pdf: + image: frooodle/s-pdf + ports: + - '8080:8080' + volumes: + - /location/of/trainingData:/usr/share/tesseract-ocr/4.00/tessdata #Required for extra OCR languages +# - /location/of/extraConfigs:/configs +# environment: +# APP_LOCALE: en_GB +# APP_HOME_NAME: Stirling PDF +# APP_HOME_DESCRIPTION: Your locally hosted one-stop-shop for all your PDF needs. +# APP_NAVBAR_NAME: Stirling PDF +# APP_ROOT_PATH: / +# ALLOW_GOOGLE_VISABILITY: true + ``` @@ -81,6 +105,14 @@ services: Please view https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md ## Want to add your own language? +Stirling PDF currently supports +- English +- Arabic (العربية) +- German (Deutsch) +- French (Français) +- Spanish (Español) +- Chinese (简体中文) + If you want to add your own language to Stirling-PDF please refer https://github.com/Frooodle/Stirling-PDF/blob/main/HowToAddNewLanguage.md @@ -98,7 +130,10 @@ Stirling PDF allows easy customization of the visible application name. Simply use environment variables APP_HOME_NAME, APP_HOME_DESCRIPTION and APP_NAVBAR_NAME with Docker or Java. If running Java directly, you can also pass these as properties using -D arguments. -Using the same method you can also change the default language by providing APP_LOCALE with values like de-DE fr-FR or ar-AR to select your default language (Will always default to English on invalid locale) +Using the same method you can also change +- The default language by providing APP_LOCALE with values like de-DE fr-FR or ar-AR to select your default language (Will always default to English on invalid locale) +- Enable/Disable search engine visablility with ALLOW_GOOGLE_VISABILITY with true / false values. Default disable visability. +- Change root URI for Stirling-PDF ie change server.com/ to server.com/pdf-app by running APP_ROOT_PATH as pdf-app ## API For those wanting to use Stirling-PDFs backend API to link with their own custom scripting to edit PDFs you can view all existing API documentation diff --git a/build.gradle b/build.gradle index 76296dbc..ff8a5f94 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { } group = 'stirling.software' -version = '0.7.0' +version = '0.8.0' sourceCompatibility = '17' repositories { diff --git a/scripts/detect-blank-pages.py b/scripts/detect-blank-pages.py new file mode 100644 index 00000000..39a065cd --- /dev/null +++ b/scripts/detect-blank-pages.py @@ -0,0 +1,40 @@ +import cv2 +import numpy as np +import sys +import argparse + +def is_blank_image(image_path, threshold=10, white_percent=99, white_value=255, blur_size=5): + image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) + + if image is None: + print(f"Error: Unable to read the image file: {image_path}") + return False + + # Apply Gaussian blur to reduce noise + blurred_image = cv2.GaussianBlur(image, (blur_size, blur_size), 0) + + _, thresholded_image = cv2.threshold(blurred_image, white_value - threshold, white_value, cv2.THRESH_BINARY) + + # Calculate the percentage of white pixels in the thresholded image + white_pixels = np.sum(thresholded_image == white_value) + total_pixels = thresholded_image.size + white_pixel_percentage = (white_pixels / total_pixels) * 100 + print(f"Page has white pixel percent of {white_pixel_percentage}") + return white_pixel_percentage > white_percent + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Detect if an image is considered blank or not.') + parser.add_argument('image_path', help='The path to the image file.') + parser.add_argument('-t', '--threshold', type=int, default=10, help='Threshold for determining white pixels. The default value is 10.') + parser.add_argument('-w', '--white_percent', type=float, default=99, help='The percentage of white pixels for an image to be considered blank. The default value is 99.') + args = parser.parse_args() + + blank = is_blank_image(args.image_path, args.threshold, args.white_percent) + + if blank: + # Return code 1: The image is considered blank. + sys.exit(1) + else: + # Return code 0: The image is not considered blank. + sys.exit(0) diff --git a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java index 6935f60f..0ab63952 100644 --- a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java +++ b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java @@ -6,15 +6,17 @@ import org.springframework.context.annotation.Configuration; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.info.License; @Configuration public class OpenApiConfig { @Bean public OpenAPI customOpenAPI() { + String version = getClass().getPackage().getImplementationVersion(); + version = (version != null) ? version : "1.0.0"; + return new OpenAPI().components(new Components()).info( - new Info().title("Your API Title").version("1.0.0").description("Your API Description").license(new License().name("Your License Name").url("Your License URL"))); + new Info().title("Stirling PDF API").version(version).description("API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")); } } 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 4a9cb2fb..ed68aeb5 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/MergeController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/MergeController.java @@ -15,6 +15,8 @@ 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 stirling.software.SPDF.utils.PdfUtils; @RestController @@ -43,8 +45,15 @@ public class MergeController { } @PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs") - public ResponseEntity mergePdfs(@RequestPart(required = true, value = "fileInput") MultipartFile[] files) throws IOException { - // Read the input PDF files into PDDocument objects + @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." + ) + public ResponseEntity mergePdfs( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input PDF files to be merged into a single file", required = true) + MultipartFile[] files) throws IOException { + // Read the input PDF files into PDDocument objects List documents = new ArrayList<>(); // Loop through the files array and read each file into a PDDocument 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 4ed72e92..9fdf2d0b 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java @@ -15,6 +15,8 @@ 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 stirling.software.SPDF.utils.PdfUtils; @RestController @@ -22,9 +24,17 @@ public class RearrangePagesPDFController { private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class); + @PostMapping(consumes = "multipart/form-data", value = "/remove-pages") - public ResponseEntity deletePages(@RequestPart(required = true, value = "fileInput") MultipartFile pdfFile, @RequestParam("pagesToDelete") String pagesToDelete) - throws IOException { + @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.") + 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) throws IOException { PDDocument document = PDDocument.load(pdfFile.getBytes()); @@ -70,7 +80,15 @@ public class RearrangePagesPDFController { } @PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages") - public ResponseEntity rearrangePages(@RequestPart(required = true, value = "fileInput") MultipartFile pdfFile, @RequestParam("pageOrder") String pageOrder) { + @Operation(summary = "Rearrange pages in a PDF file", + description = "This endpoint rearranges pages in a given PDF file based on the specified page order. Users can provide a page order as a comma-separated list of page numbers or page ranges.") + public ResponseEntity rearrangePages( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input PDF file to rearrange pages") + MultipartFile pdfFile, + @RequestParam("pageOrder") + @Parameter(description = "The new page order as a comma-separated list of page numbers or page ranges (e.g., '1,3,5-7')") + String pageOrder) { try { // Load the input PDF PDDocument document = PDDocument.load(pdfFile.getInputStream()); 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 e9d73a43..5db10a3e 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/RotationController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/RotationController.java @@ -14,6 +14,8 @@ 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 stirling.software.SPDF.utils.PdfUtils; @RestController @@ -22,8 +24,18 @@ public class RotationController { private static final Logger logger = LoggerFactory.getLogger(RotationController.class); @PostMapping(consumes = "multipart/form-data", value = "/rotate-pdf") - public ResponseEntity rotatePDF(@RequestPart(required = true, value = "fileInput") MultipartFile pdfFile, @RequestParam("angle") Integer angle) throws IOException { - + @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." + ) + public ResponseEntity rotatePDF( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The PDF file to be rotated", required = true) + MultipartFile pdfFile, + @RequestParam("angle") + @Parameter(description = "The angle by which to rotate the PDF file. This should be a multiple of 90.", example = "90", required = true) + Integer angle) throws IOException { + // Load the PDF document PDDocument document = PDDocument.load(pdfFile.getBytes()); 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 f6deb80c..99152e69 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java @@ -27,13 +27,24 @@ 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; + @RestController public class SplitPDFController { private static final Logger logger = LoggerFactory.getLogger(SplitPDFController.class); @PostMapping(consumes = "multipart/form-data", value = "/split-pages") - public ResponseEntity splitPdf(@RequestPart(required = true, value = "fileInput") MultipartFile file, @RequestParam("pages") String pages) throws IOException { + @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.") + public ResponseEntity splitPdf( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input PDF file to be split") + MultipartFile file, + @RequestParam("pages") + @Parameter(description = "The pages to be included in separate documents. Specify individual page numbers (e.g., '1,3,5'), ranges (e.g., '1-3,5-7'), or 'all' for every page.") + String pages) throws IOException { // parse user input // open the pdf document 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 12172127..05afb751 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 @@ -17,16 +17,34 @@ 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.media.Schema; import stirling.software.SPDF.utils.PdfUtils; - @RestController public class ConvertImgPDFController { private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class); @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-img") - public ResponseEntity convertToImage(@RequestPart(required = true, value = "fileInput") MultipartFile file, @RequestParam("imageFormat") String imageFormat, - @RequestParam("singleOrMultiple") String singleOrMultiple, @RequestParam("colorType") String colorType, @RequestParam("dpi") String dpi) throws IOException { + @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.") + public ResponseEntity convertToImage( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input PDF file to be converted") + MultipartFile file, + @RequestParam("imageFormat") + @Parameter(description = "The output image format", schema = @Schema(allowableValues = {"png", "jpeg", "jpg", "gif"})) + String imageFormat, + @RequestParam("singleOrMultiple") + @Parameter(description = "Choose between a single image containing all pages or separate images for each page", schema = @Schema(allowableValues = {"single", "multiple"})) + String singleOrMultiple, + @RequestParam("colorType") + @Parameter(description = "The color type of the output image(s)", schema = @Schema(allowableValues = {"rgb", "greyscale", "blackwhite"})) + String colorType, + @RequestParam("dpi") + @Parameter(description = "The DPI (dots per inch) for the output image(s)") + String dpi) throws IOException { byte[] pdfBytes = file.getBytes(); ImageType colorTypeResult = ImageType.RGB; @@ -62,12 +80,23 @@ public class ConvertImgPDFController { } @PostMapping(consumes = "multipart/form-data", value = "/img-to-pdf") - public ResponseEntity convertToPdf(@RequestPart(required = true, value = "fileInput") MultipartFile[] file, - @RequestParam(defaultValue = "false", name = "stretchToFit") boolean stretchToFit, @RequestParam(defaultValue = "true", name = "autoRotate") boolean autoRotate) - throws IOException { + @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.") + public ResponseEntity convertToPdf( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input images to be converted to a PDF file") + MultipartFile[] file, + @RequestParam(defaultValue = "false", name = "stretchToFit") + @Parameter(description = "Whether to stretch the images to fit the PDF page or maintain the aspect ratio", example = "false") + boolean stretchToFit, + @RequestParam("colorType") + @Parameter(description = "The color type of the output image(s)", schema = @Schema(allowableValues = {"rgb", "greyscale", "blackwhite"})) + String colorType, + @RequestParam(defaultValue = "false", name = "autoRotate") + @Parameter(description = "Whether to automatically rotate the images to better fit the PDF page", example = "true") + boolean autoRotate) throws IOException { // Convert the file to PDF and get the resulting bytes - System.out.println(stretchToFit); - byte[] bytes = PdfUtils.imageToPdf(file, stretchToFit, autoRotate); + byte[] bytes = PdfUtils.imageToPdf(file, stretchToFit, autoRotate, colorType); return PdfUtils.bytesToWebResponse(bytes, file[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_coverted.pdf"); } 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 9cec9a46..039954bb 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 @@ -15,6 +15,8 @@ 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 stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.ProcessExecutor; @@ -54,8 +56,18 @@ public class ConvertOfficeController { } @PostMapping(consumes = "multipart/form-data", value = "/file-to-pdf") - public ResponseEntity processPdfWithOCR(@RequestPart(required = true, value = "fileInput") MultipartFile inputFile) throws IOException, InterruptedException { - + @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." + ) + public ResponseEntity processPdfWithOCR( + @RequestPart(required = true, value = "fileInput") + @Parameter( + description = "The input file to be converted to a PDF file using OCR", + required = true + ) + MultipartFile inputFile + ) throws IOException, InterruptedException { // unused but can start server instance if startup time is to long // LibreOfficeListener.getInstance().start(); 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 29d16495..6ac5b22d 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 @@ -9,44 +9,64 @@ 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.media.Schema; import stirling.software.SPDF.utils.PDFToFile; @RestController 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.") + 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 { + PDFToFile pdfToFile = new PDFToFile(); + return pdfToFile.processPdfToOfficeFormat(inputFile, "html", "writer_pdf_import"); + } - @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-html") - public ResponseEntity processPdfToHTML(@RequestPart(required = true, value = "fileInput") MultipartFile inputFile) throws IOException, InterruptedException { - PDFToFile pdfToFile = new PDFToFile(); - return pdfToFile.processPdfToOfficeFormat(inputFile, "html", "writer_pdf_import"); - } + @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.") + 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 = { + "ppt", "pptx", "odp" })) String outputFormat) + throws IOException, InterruptedException { + PDFToFile pdfToFile = new PDFToFile(); + return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import"); + } - @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-presentation") - public ResponseEntity processPdfToPresentation(@RequestPart(required = true, value = "fileInput") MultipartFile inputFile, - @RequestParam("outputFormat") String outputFormat) throws IOException, InterruptedException { - PDFToFile pdfToFile = new PDFToFile(); - return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import"); - } + @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.") + 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 = { + "rtf", "txt:Text" })) String outputFormat) + throws IOException, InterruptedException { + PDFToFile pdfToFile = new PDFToFile(); + return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); + } - @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-text") - public ResponseEntity processPdfToRTForTXT(@RequestPart(required = true, value = "fileInput") MultipartFile inputFile, - @RequestParam("outputFormat") String outputFormat) throws IOException, InterruptedException { - PDFToFile pdfToFile = new PDFToFile(); - return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); - } + @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.") + 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 = { + "doc", "docx", "odt" })) String outputFormat) + throws IOException, InterruptedException { + PDFToFile pdfToFile = new PDFToFile(); + return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); + } - @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-word") - public ResponseEntity processPdfToWord(@RequestPart(required = true, value = "fileInput") MultipartFile inputFile, @RequestParam("outputFormat") String outputFormat) - throws IOException, InterruptedException { - PDFToFile pdfToFile = new PDFToFile(); - return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); - } + @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.") + 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 { - @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-xml") - public ResponseEntity processPdfToXML(@RequestPart(required = true, value = "fileInput") MultipartFile inputFile) throws IOException, InterruptedException { - PDFToFile pdfToFile = new PDFToFile(); - return pdfToFile.processPdfToOfficeFormat(inputFile, "xml", "writer_pdf_import"); - } + PDFToFile pdfToFile = new PDFToFile(); + return pdfToFile.processPdfToOfficeFormat(inputFile, "xml", "writer_pdf_import"); + } } 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 e36b15c6..207ed0bb 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 @@ -12,14 +12,23 @@ 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 stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.ProcessExecutor; @RestController public class ConvertPDFToPDFA { - @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-pdfa") - public ResponseEntity pdfToPdfA(@RequestPart(required = true, value = "fileInput") MultipartFile inputFile) throws IOException, InterruptedException { + @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." + ) + public ResponseEntity pdfToPdfA( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input PDF file to be converted to a PDF/A file", required = true) + MultipartFile inputFile) throws IOException, InterruptedException { // Save the uploaded file to a temporary location Path tempInputFile = Files.createTempFile("input_", ".pdf"); 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 new file mode 100644 index 00000000..98d710d7 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/other/BlankPageController.java @@ -0,0 +1,128 @@ +package stirling.software.SPDF.controller.api.other; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import javax.imageio.ImageIO; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageTree; +import org.apache.pdfbox.rendering.PDFRenderer; +import org.apache.pdfbox.text.PDFTextStripper; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import stirling.software.SPDF.utils.ImageFinder; +import stirling.software.SPDF.utils.PdfUtils; +import stirling.software.SPDF.utils.ProcessExecutor; + +@RestController +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." + ) + public ResponseEntity removeBlankPages( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input PDF file from which blank pages will be removed", required = true) + MultipartFile inputFile, + @RequestParam(defaultValue = "10", name = "threshold") + @Parameter(description = "The threshold value to determine blank pages", example = "10") + int threshold, + @RequestParam(defaultValue = "99.9", name = "whitePercent") + @Parameter(description = "The percentage of white color on a page to consider it as blank", example = "99.9") + float whitePercent) throws IOException, InterruptedException { + + PDDocument document = null; + try { + document = PDDocument.load(inputFile.getInputStream()); + PDPageTree pages = document.getDocumentCatalog().getPages(); + PDFTextStripper textStripper = new PDFTextStripper(); + + List pagesToKeepIndex = new ArrayList<>(); + int pageIndex = 0; + PDFRenderer pdfRenderer = new PDFRenderer(document); + + for (PDPage page : pages) { + System.out.println("checking page " + pageIndex); + textStripper.setStartPage(pageIndex + 1); + textStripper.setEndPage(pageIndex + 1); + String pageText = textStripper.getText(document); + boolean hasText = !pageText.trim().isEmpty(); + if (hasText) { + pagesToKeepIndex.add(pageIndex); + System.out.println("page " + pageIndex + " has text"); + } else { + boolean hasImages = hasImagesOnPage(page); + if (hasImages) { + System.out.println("page " + pageIndex + " has image"); + + Path tempFile = Files.createTempFile("image_", ".png"); + + // Render image and save as temp file + BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 300); + ImageIO.write(image, "png", tempFile.toFile()); + + List command = new ArrayList<>(Arrays.asList("python3", System.getProperty("user.dir") + "scripts/detect-blank-pages.py", tempFile.toString() ,"--threshold", String.valueOf(threshold), "--white_percent", String.valueOf(whitePercent))); + + // Run CLI command + int returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV).runCommandWithOutputHandling(command); + + // does contain data + if (returnCode == 0) { + System.out.println("page " + pageIndex + " has image which is not blank"); + pagesToKeepIndex.add(pageIndex); + } else { + System.out.println("Skipping, Image was blank for page #" + pageIndex); + } + } + } + pageIndex++; + + } + System.out.print("pagesToKeep=" + pagesToKeepIndex.size()); + + // Remove pages not present in pagesToKeepIndex + List pageIndices = IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList()); + Collections.reverse(pageIndices); // Reverse to prevent index shifting during removal + for (Integer i : pageIndices) { + if (!pagesToKeepIndex.contains(i)) { + pages.remove(i); + } + } + + return PdfUtils.pdfDocToWebResponse(document, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_blanksRemoved.pdf"); + } catch (IOException e) { + e.printStackTrace(); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } finally { + if (document != null) + document.close(); + } + } + + + 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 6a9a0d8d..89f00981 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 @@ -15,18 +15,36 @@ 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 stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.ProcessExecutor; - +import io.swagger.v3.oas.annotations.media.Schema; @RestController public class CompressController { private static final Logger logger = LoggerFactory.getLogger(CompressController.class); @PostMapping(consumes = "multipart/form-data", value = "/compress-pdf") - public ResponseEntity optimizePdf(@RequestPart(required = true, value = "fileInput") MultipartFile inputFile, @RequestParam("optimizeLevel") int optimizeLevel, - @RequestParam(name = "fastWebView", required = false) Boolean fastWebView, @RequestParam(name = "jbig2Lossy", required = false) Boolean jbig2Lossy) - throws IOException, InterruptedException { + @Operation( + summary = "Optimize PDF file", + description = "This endpoint accepts a PDF file and optimizes it based on the provided parameters." + ) + public ResponseEntity optimizePdf( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input PDF file to be optimized.", required = true) + MultipartFile inputFile, + @RequestParam("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 = {"0", "1", "2", "3"}), example = "1") + int optimizeLevel, + @RequestParam(name = "fastWebView", required = false) + @Parameter(description = "If true, optimize the PDF for fast web view. This increases the file size by about 25%.", example = "false") + Boolean fastWebView, + @RequestParam(name = "jbig2Lossy", required = false) + @Parameter(description = "If true, apply lossy JB2 compression to the PDF file.", example = "false") + Boolean jbig2Lossy) + throws IOException, InterruptedException { // Save the uploaded file to a temporary location Path tempInputFile = Files.createTempFile("input_", ".pdf"); 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 987970ba..1f6d1c5a 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 @@ -29,6 +29,8 @@ 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 stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.ProcessExecutor; @@ -38,10 +40,27 @@ public class ExtractImageScansController { private static final Logger logger = LoggerFactory.getLogger(ExtractImageScansController.class); @PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans") - public ResponseEntity extractImageScans(@RequestPart(required = true, value = "fileInput") MultipartFile inputFile, - @RequestParam(name = "angle_threshold", defaultValue = "5") int angleThreshold, @RequestParam(name = "tolerance", defaultValue = "20") int tolerance, - @RequestParam(name = "min_area", defaultValue = "8000") int minArea, @RequestParam(name = "min_contour_area", defaultValue = "500") int minContourArea, - @RequestParam(name = "border_size", defaultValue = "1") int borderSize) throws IOException, InterruptedException { + @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.") + public ResponseEntity extractImageScans( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input file containing image scans") + MultipartFile inputFile, + @RequestParam(name = "angle_threshold", defaultValue = "5") + @Parameter(description = "The angle threshold for the image scan extraction", example = "5") + int angleThreshold, + @RequestParam(name = "tolerance", defaultValue = "20") + @Parameter(description = "The tolerance for the image scan extraction", example = "20") + int tolerance, + @RequestParam(name = "min_area", defaultValue = "8000") + @Parameter(description = "The minimum area for the image scan extraction", example = "8000") + int minArea, + @RequestParam(name = "min_contour_area", defaultValue = "500") + @Parameter(description = "The minimum contour area for the image scan extraction", example = "500") + int minContourArea, + @RequestParam(name = "border_size", defaultValue = "1") + @Parameter(description = "The border size for the image scan extraction", example = "1") + int borderSize) throws IOException, InterruptedException { String fileName = inputFile.getOriginalFilename(); String extension = fileName.substring(fileName.lastIndexOf(".") + 1); @@ -82,8 +101,18 @@ public class ExtractImageScansController { for (int i = 0; i < images.size(); i++) { Path tempDir = Files.createTempDirectory("openCV_output"); - List command = new ArrayList<>(Arrays.asList("python3", "/scripts/split_photos.py", images.get(i), tempDir.toString(), String.valueOf(angleThreshold), - String.valueOf(tolerance), String.valueOf(minArea), String.valueOf(minContourArea), String.valueOf(borderSize))); + List command = new ArrayList<>(Arrays.asList( + "python3", + "./scripts/split_photos.py", + images.get(i), + tempDir.toString(), + "--angle_threshold", String.valueOf(angleThreshold), + "--tolerance", String.valueOf(tolerance), + "--min_area", String.valueOf(minArea), + "--min_contour_area", String.valueOf(minContourArea), + "--border_size", String.valueOf(borderSize) + )); + // Run CLI command int returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV).runCommandWithOutputHandling(command); 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 d935bd39..cf31dda7 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 @@ -26,15 +26,25 @@ 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.media.Schema; import stirling.software.SPDF.utils.PdfUtils; - @RestController public class ExtractImagesController { private static final Logger logger = LoggerFactory.getLogger(ExtractImagesController.class); @PostMapping(consumes = "multipart/form-data", value = "/extract-images") - public ResponseEntity extractImages(@RequestPart(required = true, value = "fileInput") MultipartFile file, @RequestParam("format") String format) throws IOException { + @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.") + public ResponseEntity extractImages( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input PDF file containing images") + MultipartFile file, + @RequestParam("format") + @Parameter(description = "The output image format e.g., 'png', 'jpeg', or 'gif'", schema = @Schema(allowableValues = {"png", "jpeg", "gif"})) + String format) throws IOException { System.out.println(System.currentTimeMillis() + "file=" + file.getName() + ", format=" + format); PDDocument document = PDDocument.load(file.getBytes()); 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 42309c39..5840cd64 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 @@ -17,6 +17,8 @@ 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 stirling.software.SPDF.utils.PdfUtils; @RestController @@ -35,13 +37,44 @@ public class MetadataController { } @PostMapping(consumes = "multipart/form-data", value = "/update-metadata") - public ResponseEntity metadata(@RequestPart(required = true, value = "fileInput") MultipartFile pdfFile, - @RequestParam(value = "deleteAll", required = false, defaultValue = "false") Boolean deleteAll, @RequestParam(value = "author", required = false) String author, - @RequestParam(value = "creationDate", required = false) String creationDate, @RequestParam(value = "creator", required = false) String creator, - @RequestParam(value = "keywords", required = false) String keywords, @RequestParam(value = "modificationDate", required = false) String modificationDate, - @RequestParam(value = "producer", required = false) String producer, @RequestParam(value = "subject", required = false) String subject, - @RequestParam(value = "title", required = false) String title, @RequestParam(value = "trapped", required = false) String trapped, - @RequestParam Map allRequestParams) throws IOException { + @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.") + public ResponseEntity metadata( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input PDF file to update metadata") + MultipartFile pdfFile, + @RequestParam(value = "deleteAll", required = false, defaultValue = "false") + @Parameter(description = "Delete all metadata if set to true") + Boolean deleteAll, + @RequestParam(value = "author", required = false) + @Parameter(description = "The author of the document") + String author, + @RequestParam(value = "creationDate", required = false) + @Parameter(description = "The creation date of the document (format: yyyy/MM/dd HH:mm:ss)") + String creationDate, + @RequestParam(value = "creator", required = false) + @Parameter(description = "The creator of the document") + String creator, + @RequestParam(value = "keywords", required = false) + @Parameter(description = "The keywords for the document") + String keywords, + @RequestParam(value = "modificationDate", required = false) + @Parameter(description = "The modification date of the document (format: yyyy/MM/dd HH:mm:ss)") + String modificationDate, + @RequestParam(value = "producer", required = false) + @Parameter(description = "The producer of the document") + String producer, + @RequestParam(value = "subject", required = false) + @Parameter(description = "The subject of the document") + String subject, + @RequestParam(value = "title", required = false) + @Parameter(description = "The title of the document") + String title, + @RequestParam(value = "trapped", required = false) + @Parameter(description = "The trapped status of the document") + String trapped, + @RequestParam Map allRequestParams) + throws IOException { // Load the PDF file into a PDDocument PDDocument document = PDDocument.load(pdfFile.getBytes()); 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 5c7f2553..f1769dce 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 @@ -24,6 +24,9 @@ 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.media.Schema; import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.ProcessExecutor; @@ -43,13 +46,36 @@ public class OCRController { } @PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf") - public ResponseEntity processPdfWithOCR(@RequestPart(required = true, value = "fileInput") MultipartFile inputFile, - @RequestParam("languages") List selectedLanguages, @RequestParam(name = "sidecar", required = false) Boolean sidecar, - @RequestParam(name = "deskew", required = false) Boolean deskew, @RequestParam(name = "clean", required = false) Boolean clean, - @RequestParam(name = "clean-final", required = false) Boolean cleanFinal, @RequestParam(name = "ocrType", required = false) String ocrType, - @RequestParam(name = "ocrRenderType", required = false, defaultValue = "hocr") String ocrRenderType, - @RequestParam(name = "removeImagesAfter", required = false) Boolean removeImagesAfter) - throws IOException, InterruptedException { + @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.") + public ResponseEntity processPdfWithOCR( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input PDF file to be processed with OCR") + MultipartFile inputFile, + @RequestParam("languages") + @Parameter(description = "List of languages to use in OCR processing") + List selectedLanguages, + @RequestParam(name = "sidecar", required = false) + @Parameter(description = "Include OCR text in a sidecar text file if set to true") + Boolean sidecar, + @RequestParam(name = "deskew", required = false) + @Parameter(description = "Deskew the input file if set to true") + Boolean deskew, + @RequestParam(name = "clean", required = false) + @Parameter(description = "Clean the input file if set to true") + Boolean clean, + @RequestParam(name = "clean-final", required = false) + @Parameter(description = "Clean the final output if set to true") + Boolean cleanFinal, + @RequestParam(name = "ocrType", required = false) + @Parameter(description = "Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'", schema = @Schema(allowableValues = {"skip-text", "force-ocr", "Normal"})) + String ocrType, + @RequestParam(name = "ocrRenderType", required = false, defaultValue = "hocr") + @Parameter(description = "Specify the OCR render type, either 'hocr' or 'sandwich'", schema = @Schema(allowableValues = {"hocr", "sandwich"})) + String ocrRenderType, + @RequestParam(name = "removeImagesAfter", required = false) + @Parameter(description = "Remove images from the output PDF if set to true") + Boolean removeImagesAfter) throws IOException, InterruptedException { // --output-type pdfa if (selectedLanguages == null || selectedLanguages.isEmpty()) { 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 4b0d4017..0bf8ce83 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 @@ -12,6 +12,8 @@ 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 stirling.software.SPDF.utils.PdfUtils; @RestController @@ -20,8 +22,26 @@ public class OverlayImageController { private static final Logger logger = LoggerFactory.getLogger(OverlayImageController.class); @PostMapping(consumes = "multipart/form-data", value = "/add-image") - public ResponseEntity overlayImage(@RequestPart(required = true, value = "fileInput") MultipartFile pdfFile, @RequestParam("fileInput2") MultipartFile imageFile, - @RequestParam("x") float x, @RequestParam("y") float y, @RequestParam("everyPage") boolean everyPage) { + @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." + ) + public ResponseEntity overlayImage( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input PDF file to overlay the image onto.", required = true) + MultipartFile pdfFile, + @RequestParam("fileInput2") + @Parameter(description = "The image file to be overlaid onto the PDF.", required = true) + MultipartFile imageFile, + @RequestParam("x") + @Parameter(description = "The x-coordinate at which to place the top-left corner of the image.", example = "0") + float x, + @RequestParam("y") + @Parameter(description = "The y-coordinate at which to place the top-left corner of the image.", example = "0") + float y, + @RequestParam("everyPage") + @Parameter(description = "Whether to overlay the image onto every page of the PDF.", example = "false") + boolean everyPage) { try { byte[] pdfBytes = pdfFile.getBytes(); byte[] imageBytes = imageFile.getBytes(); 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 new file mode 100644 index 00000000..55cdf83e --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/other/RepairController.java @@ -0,0 +1,66 @@ +package stirling.software.SPDF.controller.api.other; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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 stirling.software.SPDF.utils.PdfUtils; +import stirling.software.SPDF.utils.ProcessExecutor; + +@RestController +public class RepairController { + + private static final Logger logger = LoggerFactory.getLogger(RepairController.class); + + @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." + ) + public ResponseEntity repairPdf( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input PDF file to be repaired", required = true) + MultipartFile inputFile) throws IOException, InterruptedException { + + // Save the uploaded file to a temporary location + Path tempInputFile = Files.createTempFile("input_", ".pdf"); + inputFile.transferTo(tempInputFile.toFile()); + + // Prepare the output file path + Path tempOutputFile = Files.createTempFile("output_", ".pdf"); + + List command = new ArrayList<>(); + command.add("gs"); + command.add("-o"); + command.add(tempOutputFile.toString()); + command.add("-sDEVICE=pdfwrite"); + command.add(tempInputFile.toString()); + + + int returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(command); + + // Read the optimized PDF file + byte[] pdfBytes = Files.readAllBytes(tempOutputFile); + + // Clean up the temporary files + Files.delete(tempInputFile); + Files.delete(tempOutputFile); + + // Return the optimized PDF as a response + String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_repaired.pdf"; + return PdfUtils.bytesToWebResponse(pdfBytes, outputFilename); + } + +} 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 3cbe9965..1872d6eb 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 @@ -14,8 +14,10 @@ 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 stirling.software.SPDF.utils.PdfUtils; - +import io.swagger.v3.oas.annotations.media.Schema; @RestController public class PasswordController { @@ -23,23 +25,62 @@ public class PasswordController { @PostMapping(consumes = "multipart/form-data", value = "/remove-password") - public ResponseEntity compressPDF(@RequestPart(required = true, value = "fileInput") MultipartFile fileInput, @RequestParam(name = "password") String password) - throws IOException { + @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." + ) + public ResponseEntity removePassword( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input PDF file from which the password should be removed", required = true) + MultipartFile fileInput, + @RequestParam(name = "password") + @Parameter(description = "The password of the PDF file", required = true) + String password) throws IOException { PDDocument document = PDDocument.load(fileInput.getBytes(), password); document.setAllSecurityToBeRemoved(true); return PdfUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_password_removed.pdf"); } @PostMapping(consumes = "multipart/form-data", value = "/add-password") - public ResponseEntity compressPDF(@RequestPart(required = true, value = "fileInput") MultipartFile fileInput, - @RequestParam(defaultValue = "", name = "password") String password, @RequestParam(defaultValue = "128", name = "keyLength") int keyLength, - @RequestParam(defaultValue = "false", name = "canAssembleDocument") boolean canAssembleDocument, - @RequestParam(defaultValue = "false", name = "canExtractContent") boolean canExtractContent, - @RequestParam(defaultValue = "false", name = "canExtractForAccessibility") boolean canExtractForAccessibility, - @RequestParam(defaultValue = "false", name = "canFillInForm") boolean canFillInForm, @RequestParam(defaultValue = "false", name = "canModify") boolean canModify, - @RequestParam(defaultValue = "false", name = "canModifyAnnotations") boolean canModifyAnnotations, - @RequestParam(defaultValue = "false", name = "canPrint") boolean canPrint, @RequestParam(defaultValue = "false", name = "canPrintFaithful") boolean canPrintFaithful) - throws IOException { + @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." + ) + public ResponseEntity addPassword( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input PDF file to which the password should be added", required = true) + MultipartFile fileInput, + @RequestParam(defaultValue = "", name = "password") + @Parameter(description = "The password to be added to the PDF file") + String password, + @RequestParam(defaultValue = "128", name = "keyLength") + @Parameter(description = "The length of the encryption key", schema = @Schema(allowableValues = {"40", "128", "256"})) + int keyLength, + @RequestParam(defaultValue = "false", name = "canAssembleDocument") + @Parameter(description = "Whether the document assembly is allowed", example = "false") + boolean canAssembleDocument, + @RequestParam(defaultValue = "false", name = "canExtractContent") + @Parameter(description = "Whether content extraction for accessibility is allowed", example = "false") + boolean canExtractContent, + @RequestParam(defaultValue = "false", name = "canExtractForAccessibility") + @Parameter(description = "Whether content extraction for accessibility is allowed", example = "false") + boolean canExtractForAccessibility, + @RequestParam(defaultValue = "false", name = "canFillInForm") + @Parameter(description = "Whether form filling is allowed", example = "false") + boolean canFillInForm, + @RequestParam(defaultValue = "false", name = "canModify") + @Parameter(description = "Whether the document modification is allowed", example = "false") + boolean canModify, + @RequestParam(defaultValue = "false", name = "canModifyAnnotations") + @Parameter(description = "Whether modification of annotations is allowed", example = "false") + boolean canModifyAnnotations, + @RequestParam(defaultValue = "false", name = "canPrint") + @Parameter(description = "Whether printing of the document is allowed", example = "false") + boolean canPrint, + @RequestParam(defaultValue = "false", name = "canPrintFaithful") + @Parameter(description = "Whether faithful printing is allowed", example = "false") + boolean canPrintFaithful + ) throws IOException { PDDocument document = PDDocument.load(fileInput.getBytes()); AccessPermission ap = new AccessPermission(); 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 8f9203b1..46199c63 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 @@ -2,20 +2,13 @@ package stirling.software.SPDF.controller.api.security; import java.awt.Color; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDDocumentCatalog; 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.PDType1Font; import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationMarkup; -import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; -import org.apache.pdfbox.pdmodel.interactive.form.PDField; import org.apache.pdfbox.util.Matrix; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -24,17 +17,38 @@ 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 stirling.software.SPDF.utils.PdfUtils; -import stirling.software.SPDF.utils.WatermarkRemover; @RestController public class WatermarkController { @PostMapping(consumes = "multipart/form-data", value = "/add-watermark") - public ResponseEntity addWatermark(@RequestPart(required = true, value = "fileInput") MultipartFile pdfFile, @RequestParam("watermarkText") String watermarkText, - @RequestParam(defaultValue = "30", name = "fontSize") float fontSize, @RequestParam(defaultValue = "0", name = "rotation") float rotation, - @RequestParam(defaultValue = "0.5", name = "opacity") float opacity, @RequestParam(defaultValue = "50", name = "widthSpacer") int widthSpacer, - @RequestParam(defaultValue = "50", name = "heightSpacer") int heightSpacer) throws IOException { + @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.") + public ResponseEntity addWatermark( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input PDF file to add a watermark") + MultipartFile pdfFile, + @RequestParam("watermarkText") + @Parameter(description = "The watermark text to add to the PDF file") + String watermarkText, + @RequestParam(defaultValue = "30", name = "fontSize") + @Parameter(description = "The font size of the watermark text", example = "30") + float fontSize, + @RequestParam(defaultValue = "0", name = "rotation") + @Parameter(description = "The rotation of the watermark text in degrees", example = "0") + float rotation, + @RequestParam(defaultValue = "0.5", name = "opacity") + @Parameter(description = "The opacity of the watermark text (0.0 - 1.0)", example = "0.5") + float opacity, + @RequestParam(defaultValue = "50", name = "widthSpacer") + @Parameter(description = "The width spacer between watermark texts", example = "50") + int widthSpacer, + @RequestParam(defaultValue = "50", name = "heightSpacer") + @Parameter(description = "The height spacer between watermark texts", example = "50") + int heightSpacer) throws IOException { // Load the input PDF PDDocument document = PDDocument.load(pdfFile.getInputStream()); @@ -80,61 +94,4 @@ public class WatermarkController { return PdfUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf"); } - - @PostMapping(consumes = "multipart/form-data", value = "/remove-watermark") - public ResponseEntity removeWatermark(@RequestPart(required = true, value = "fileInput") MultipartFile pdfFile, @RequestParam("watermarkText") String watermarkText) - throws Exception { - - // Load the input PDF - PDDocument document = PDDocument.load(pdfFile.getInputStream()); - - // Create a new PDF document for the output - PDDocument outputDocument = new PDDocument(); - - // Loop through the pages - int numPages = document.getNumberOfPages(); - for (int i = 0; i < numPages; i++) { - PDPage page = document.getPage(i); - - // Process the content stream to remove the watermark text - WatermarkRemover editor = new WatermarkRemover(watermarkText) { - }; - editor.processPage(page); - editor.processPage(page); - // Add the page to the output document - outputDocument.addPage(page); - } - - for (PDPage page : outputDocument.getPages()) { - List annotations = page.getAnnotations(); - List annotationsToRemove = new ArrayList<>(); - - for (PDAnnotation annotation : annotations) { - if (annotation instanceof PDAnnotationMarkup) { - PDAnnotationMarkup markup = (PDAnnotationMarkup) annotation; - String contents = markup.getContents(); - if (contents != null && contents.contains(watermarkText)) { - annotationsToRemove.add(markup); - } - } - } - - annotations.removeAll(annotationsToRemove); - } - PDDocumentCatalog catalog = outputDocument.getDocumentCatalog(); - PDAcroForm acroForm = catalog.getAcroForm(); - if (acroForm != null) { - List fields = acroForm.getFields(); - for (PDField field : fields) { - String fieldValue = field.getValueAsString(); - if (fieldValue.contains(watermarkText)) { - field.setValue(fieldValue.replace(watermarkText, "")); - } - } - } - - return PdfUtils.pdfDocToWebResponse(outputDocument, "removed.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 14ec0be6..aca36980 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java @@ -1,8 +1,10 @@ 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; @@ -66,4 +68,28 @@ public class GeneralWebController { model.addAttribute("currentPage", "split-pdfs"); return "split-pdfs"; } + + @GetMapping("/sign") + @Hidden + public String signForm(Model model) { + model.addAttribute("currentPage", "sign"); + return "sign"; + } + + @GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE) + @ResponseBody + @Hidden + public String getRobotsTxt() { + String allowGoogleVisibility = System.getProperty("ALLOW_GOOGLE_VISABILITY"); + if (allowGoogleVisibility == null) + allowGoogleVisibility = System.getenv("ALLOW_GOOGLE_VISABILITY"); + 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/OtherWebController.java b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java index a7d17d76..aa4fbbf2 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java @@ -37,6 +37,14 @@ public class OtherWebController { return "other/extract-images"; } + @GetMapping("/flatten") + @Hidden + public String flattenForm(Model model) { + model.addAttribute("currentPage", "flatten"); + return "other/flatten"; + } + + @GetMapping("/change-metadata") @Hidden @@ -45,6 +53,12 @@ public class OtherWebController { return "other/change-metadata"; } + @GetMapping("/compare") + @Hidden + public String compareForm(Model model) { + model.addAttribute("currentPage", "compare"); + return "other/compare"; + } public List getAvailableTesseractLanguages() { String tessdataDir = "/usr/share/tesseract-ocr/4.00/tessdata"; @@ -80,5 +94,18 @@ public class OtherWebController { return "other/adjust-contrast"; } + @GetMapping("/repair") + @Hidden + public String repairForm(Model model) { + model.addAttribute("currentPage", "repair"); + return "other/repair"; + } + + @GetMapping("/remove-blanks") + @Hidden + public String removeBlanksForm(Model model) { + model.addAttribute("currentPage", "remove-blanks"); + return "other/remove-blanks"; + } } 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 31f16563..9eb10267 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java @@ -34,13 +34,4 @@ public class SecurityWebController { model.addAttribute("currentPage", "add-watermark"); return "security/add-watermark"; } - - //WIP - @GetMapping("/remove-watermark") - @Hidden - public String removeWatermarkForm(Model model) { - model.addAttribute("currentPage", "remove-watermark"); - return "security/remove-watermark"; - } - } diff --git a/src/main/java/stirling/software/SPDF/utils/ImageFinder.java b/src/main/java/stirling/software/SPDF/utils/ImageFinder.java new file mode 100644 index 00000000..9d5ad08e --- /dev/null +++ b/src/main/java/stirling/software/SPDF/utils/ImageFinder.java @@ -0,0 +1,130 @@ +package stirling.software.SPDF.utils; + +import java.awt.geom.Point2D; +import java.io.IOException; +import java.util.List; + +import org.apache.pdfbox.contentstream.operator.Operator; +import org.apache.pdfbox.contentstream.operator.OperatorName; +import org.apache.pdfbox.cos.COSBase; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.graphics.PDXObject; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.pdmodel.graphics.image.PDImage; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; + +public class ImageFinder extends org.apache.pdfbox.contentstream.PDFGraphicsStreamEngine { + private boolean hasImages = false; + + public ImageFinder(PDPage page) { + super(page); + } + + public boolean hasImages() { + return hasImages; + } + + @Override + protected void processOperator(Operator operator, List operands) throws IOException { + String operation = operator.getName(); + if (operation.equals(OperatorName.DRAW_OBJECT)) { + COSBase base = operands.get(0); + if (base instanceof COSName) { + COSName objectName = (COSName) base; + PDXObject xobject = getResources().getXObject(objectName); + if (xobject instanceof PDImageXObject) { + hasImages = true; + } else if (xobject instanceof PDFormXObject) { + PDFormXObject form = (PDFormXObject) xobject; + ImageFinder innerFinder = new ImageFinder(getPage()); + innerFinder.processPage(getPage()); + if (innerFinder.hasImages()) { + hasImages = true; + } + } + } + } + super.processOperator(operator, operands); + } + + @Override + public void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void drawImage(PDImage pdImage) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void clip(int windingRule) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void moveTo(float x, float y) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void lineTo(float x, float y) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public Point2D getCurrentPoint() throws IOException { + // TODO Auto-generated method stub + return null; + } + + @Override + public void closePath() throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void endPath() throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void strokePath() throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void fillPath(int windingRule) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void fillAndStrokePath(int windingRule) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void shadingFill(COSName shadingName) throws IOException { + // TODO Auto-generated method stub + + } + + // ... rest of the overridden methods +} diff --git a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java index e6a1a602..2fd871b0 100644 --- a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java @@ -2,6 +2,8 @@ package stirling.software.SPDF.utils; import java.awt.Graphics; import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.awt.image.ColorConvertOp; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; @@ -24,10 +26,13 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import javax.imageio.ImageIO; +import javax.imageio.ImageReader; 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; @@ -119,99 +124,99 @@ public class PdfUtils { throw e; } } - - public static byte[] imageToPdf(MultipartFile[] files, boolean stretchToFit, boolean autoRotate) throws IOException { + public static byte[] imageToPdf(MultipartFile[] files, boolean stretchToFit, boolean autoRotate, String colorType) throws IOException { try (PDDocument doc = new PDDocument()) { for (MultipartFile file : files) { - // Create a temporary file for the image - File imageFile = Files.createTempFile("image", ".jpg").toFile(); - - try (FileOutputStream fos = new FileOutputStream(imageFile); InputStream input = file.getInputStream()) { - byte[] buffer = new byte[1024]; - int len; - // Read from the input stream and write to the file - while ((len = input.read(buffer)) != -1) { - fos.write(buffer, 0, len); + String originalFilename = file.getOriginalFilename(); + if (originalFilename != null && (originalFilename.toLowerCase().endsWith(".tiff") || originalFilename.toLowerCase().endsWith(".tif")) ) { + ImageReader reader = ImageIO.getImageReadersByFormatName("tiff").next(); + reader.setInput(ImageIO.createImageInputStream(file.getInputStream())); + int numPages = reader.getNumImages(true); + for (int i = 0; i < numPages; i++) { + BufferedImage pageImage = reader.read(i); + BufferedImage convertedImage = convertColorType(pageImage, colorType); + PDImageXObject pdImage = LosslessFactory.createFromImage(doc, convertedImage); + addImageToDocument(doc, pdImage, stretchToFit, autoRotate); } - logger.info("Image successfully written to file: {}", imageFile.getAbsolutePath()); - } catch (IOException e) { - logger.error("Error writing image to file: {}", imageFile.getAbsolutePath(), e); - throw e; - } - - // Create a new PDF page - PDPage page = new PDPage(); - doc.addPage(page); - - // Create an image object from the image file - PDImageXObject image = PDImageXObject.createFromFileByContent(imageFile, doc); - - float pageWidth = page.getMediaBox().getWidth(); - float pageHeight = page.getMediaBox().getHeight(); - - if (autoRotate && ((image.getWidth() > image.getHeight() && pageHeight > pageWidth) || (image.getWidth() < image.getHeight() && pageWidth > pageHeight))) { - // Rotate the page 90 degrees if the image better fits the page in landscape - // orientation - page.setRotation(90); - pageWidth = page.getMediaBox().getHeight(); - pageHeight = page.getMediaBox().getWidth(); - } - - try (PDPageContentStream contentStream = new PDPageContentStream(doc, page)) { - if (stretchToFit) { - if (page.getRotation() == 0 || page.getRotation() == 180) { - // Stretch the image to fit the whole page - contentStream.drawImage(image, 0, 0, pageWidth, pageHeight); - } else { - // Adjust the width and height of the page when rotated - contentStream.drawImage(image, 0, 0, pageHeight, pageWidth); + } else { + File imageFile = Files.createTempFile("image", ".png").toFile(); + try (FileOutputStream fos = new FileOutputStream(imageFile); InputStream input = file.getInputStream()) { + byte[] buffer = new byte[1024]; + int len; + while ((len = input.read(buffer)) != -1) { + fos.write(buffer, 0, len); } - logger.info("Image successfully added to PDF, stretched to fit page"); - } else { - // Ensure the image fits the page but maintain the image's aspect ratio - float imageAspectRatio = (float) image.getWidth() / (float) image.getHeight(); - float pageAspectRatio = pageWidth / pageHeight; - - // Determine the scale factor to fit the image onto the page - float scaleFactor = 1.0f; - if (imageAspectRatio > pageAspectRatio) { - // Image is wider than the page, scale to fit the width - scaleFactor = pageWidth / image.getWidth(); - } else { - // Image is taller than the page, scale to fit the height - scaleFactor = pageHeight / image.getHeight(); - } - - // Calculate the position of the image on the page - float xPos = (pageWidth - (image.getWidth() * scaleFactor)) / 2; - float yPos = (pageHeight - (image.getHeight() * scaleFactor)) / 2; - - // Draw the image onto the page - if (page.getRotation() == 0 || page.getRotation() == 180) { - contentStream.drawImage(image, xPos, yPos, image.getWidth() * scaleFactor, image.getHeight() * scaleFactor); - } else { - // Adjust the width and height of the page when rotated - contentStream.drawImage(image, yPos, xPos, image.getHeight() * scaleFactor, image.getWidth() * scaleFactor); - } - logger.info("Image successfully added to PDF, maintaining aspect ratio"); + BufferedImage image = ImageIO.read(imageFile); + BufferedImage convertedImage = convertColorType(image, colorType); + PDImageXObject pdImage = LosslessFactory.createFromImage(doc, convertedImage); + addImageToDocument(doc, pdImage, stretchToFit, autoRotate); + } catch (IOException e) { + logger.error("Error writing image to file: {}", imageFile.getAbsolutePath(), e); + throw e; + } finally { + imageFile.delete(); } - } catch (IOException e) { - logger.error("Error adding image to PDF", e); - throw e; } - - // Delete the temporary file - imageFile.delete(); } - - // Create a ByteArrayOutputStream to save the PDF to ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); doc.save(byteArrayOutputStream); logger.info("PDF successfully saved to byte array"); - return byteArrayOutputStream.toByteArray(); } + } + private static BufferedImage convertColorType(BufferedImage sourceImage, String colorType) { + BufferedImage convertedImage; + switch (colorType) { + case "greyscale": + convertedImage = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), BufferedImage.TYPE_BYTE_GRAY); + convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null); + break; + case "blackwhite": + convertedImage = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), BufferedImage.TYPE_BYTE_BINARY); + convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null); + break; + default: // full color + convertedImage = sourceImage; + break; + } + return convertedImage; + } + + private static void addImageToDocument(PDDocument doc, PDImageXObject image, boolean stretchToFit, boolean autoRotate) throws IOException { + boolean imageIsLandscape = image.getWidth() > image.getHeight(); + PDRectangle pageSize = PDRectangle.A4; + if (autoRotate && imageIsLandscape) { + pageSize = new PDRectangle(pageSize.getHeight(), pageSize.getWidth()); + } + PDPage page = new PDPage(pageSize); + doc.addPage(page); + + float pageWidth = page.getMediaBox().getWidth(); + float pageHeight = page.getMediaBox().getHeight(); + + try (PDPageContentStream contentStream = new PDPageContentStream(doc, page)) { + if (stretchToFit) { + contentStream.drawImage(image, 0, 0, pageWidth, pageHeight); + } else { + float imageAspectRatio = (float) image.getWidth() / (float) image.getHeight(); + float pageAspectRatio = pageWidth / pageHeight; + + float scaleFactor = 1.0f; + if (imageAspectRatio > pageAspectRatio) { + scaleFactor = pageWidth / image.getWidth(); + } else { + scaleFactor = pageHeight / image.getHeight(); + } + + float xPos = (pageWidth - (image.getWidth() * scaleFactor)) / 2; + float yPos = (pageHeight - (image.getHeight() * scaleFactor)) / 2; + contentStream.drawImage(image, xPos, yPos, image.getWidth() * scaleFactor, image.getHeight() * scaleFactor); + } + } catch (IOException e) { + logger.error("Error adding image to PDF", e); + throw e; + } } public static X509Certificate[] loadCertificateChainFromKeystore(InputStream keystoreInputStream, String keystorePassword) throws Exception { diff --git a/src/main/java/stirling/software/SPDF/utils/WatermarkRemover.java b/src/main/java/stirling/software/SPDF/utils/WatermarkRemover.java deleted file mode 100644 index 90b44e53..00000000 --- a/src/main/java/stirling/software/SPDF/utils/WatermarkRemover.java +++ /dev/null @@ -1,69 +0,0 @@ -package stirling.software.SPDF.utils; - -import java.io.IOException; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.apache.pdfbox.contentstream.PDFStreamEngine; -import org.apache.pdfbox.contentstream.operator.Operator; -import org.apache.pdfbox.cos.COSArray; -import org.apache.pdfbox.cos.COSBase; -import org.apache.pdfbox.cos.COSString; - -public class WatermarkRemover extends PDFStreamEngine { - - private final Pattern pattern; - private final String watermarkText; - - public WatermarkRemover(String watermarkText) { - this.watermarkText = watermarkText; - this.pattern = Pattern.compile(Pattern.quote(watermarkText)); - } - - @Override - protected void processOperator(Operator operator, List operands) throws IOException { - String operation = operator.getName(); - - boolean processText = false; - if ("Tj".equals(operation) || "TJ".equals(operation) || "'".equals(operation) || "\"".equals(operation)) { - processText = true; - } - - if (processText) { - for (int j = 0; j < operands.size(); ++j) { - COSBase operand = operands.get(j); - if (operand instanceof COSString) { - COSString cosString = (COSString) operand; - String string = cosString.getString(); - Matcher matcher = pattern.matcher(string); - if (matcher.find()) { - string = matcher.replaceAll(""); - cosString.setValue(string.getBytes()); - } - } else if (operand instanceof COSArray) { - COSArray array = (COSArray) operand; - for (int i = 0; i < array.size(); i++) { - COSBase item = array.get(i); - if (item instanceof COSString) { - COSString cosString = (COSString) item; - String string = cosString.getString(); - Matcher matcher = pattern.matcher(string); - if (matcher.find()) { - System.out.println("operation =" + operation); - System.out.println("1 =" + string); - string = matcher.replaceAll(""); - cosString.setValue(string.getBytes()); - array.set(i, cosString); - operands.set(j, array); - } - - } - } - } - - } - } - super.processOperator(operator, operands); - } -} diff --git a/src/main/resources/messages_ar_AR.properties b/src/main/resources/messages_ar_AR.properties index 15a3ee2c..c9b9938d 100644 --- a/src/main/resources/messages_ar_AR.properties +++ b/src/main/resources/messages_ar_AR.properties @@ -114,6 +114,56 @@ home.PDFToXML.desc=تحويل PDF إلى تنسيق XML home.ScannerImageSplit.title=كشف / انقسام الصور الممسوحة ضوئيًا home.ScannerImageSplit.desc=تقسيم عدة صور من داخل صورة / ملف PDF +home.sign.title = تسجيل الدخول +home.sign.desc = إضافة التوقيع إلى PDF عن طريق الرسم أو النص أو الصورة + +home.flatten.title = تسطيح +home.flatten.desc = قم بإزالة كافة العناصر والنماذج التفاعلية من ملف PDF + +home.repair.title = إصلاح +home.repair.desc = يحاول إصلاح ملف PDF تالف / معطل + +home.removeBlanks.title = إزالة الصفحات الفارغة +home.removeBlanks.desc = يكتشف ويزيل الصفحات الفارغة من المستند + +home.compare.title = قارن +home.compare.desc = يقارن ويظهر الاختلافات بين 2 من مستندات PDF + +downloadPdf = تنزيل PDF +text=نص +font=الخط + +removeBlanks.title = إزالة الفراغات +removeBlanks.header = إزالة الصفحات الفارغة +removeBlanks.threshold = العتبة: +removeBlanks.thresholdDesc = الحد الفاصل لتحديد مدى بياض البكسل الأبيض +removeBlanks.whitePercent = نسبة الأبيض (٪): +removeBlanks.whitePercentDesc = النسبة المئوية للصفحة التي يجب أن تكون بيضاء لتتم إزالتها +removeBlanks.submit = إزالة الفراغات + +compare.title=يقارن +compare.header=قارن ملفات PDF +compare.document.1=المستند 1 +compare.document.2=المستند 2 +compare.submit=يقارن + +sign.title = تسجيل الدخول +sign.header = توقيع ملفات PDF +sign.upload = تحميل الصورة +sign.draw = رسم التوقيع +Sign.text = إدخال النص + +sign.clear=واضح +sign.add = إضافة + +repair.title = إصلاح +repair.header = إصلاح ملفات PDF +repair.submit = الإصلاح + +flatten.title = تسطيح +flatten.header = تسوية ملفات PDF +flatten.submit = تسطيح + ScannerImageSplit.selectText.1=عتبة الزاوية: ScannerImageSplit.selectText.2=تعيين الحد الأدنى للزاوية المطلقة المطلوبة لتدوير الصورة (افتراضي: 10). ScannerImageSplit.selectText.3=التسامح: @@ -171,7 +221,7 @@ fileToPDF.submit=\u062A\u062D\u0648\u064A\u0644 \u0625\u0644\u0649 PDF #Add image addImage.title=إضافة صورة -addImage.header=إضافة صورة إلى PDF (العمل قيد التقدم) +addImage.header=إضافة صورة إلى PDF addImage.everyPage=كل صفحة؟ addImage.submit=إضافة صورة diff --git a/src/main/resources/messages_ca_CA.properties b/src/main/resources/messages_ca_CA.properties index a61d87ff..95b60e50 100644 --- a/src/main/resources/messages_ca_CA.properties +++ b/src/main/resources/messages_ca_CA.properties @@ -108,7 +108,54 @@ home.PDFToXML.desc=Converteix PDF a format XML home.ScannerImageSplit.title=Detecta/Divideix fotos escanejades home.ScannerImageSplit.desc=Divideix múltiples fotos dins del PDF/foto +home.sign.title=Sign +home.sign.desc=Afegeix signatura al PDF mitjançant dibuix, text o imatge +home.flatten.title=Aplanar +home.flatten.desc=Elimineu tots els elements i formularis interactius d'un PDF + +home.repair.title=Reparar +home.repair.desc=Intenta reparar un PDF danyat o trencat + +home.removeBlanks.title=Elimina les pàgines en blanc +home.removeBlanks.desc=Detecta i elimina les pàgines en blanc d'un document + +home.compare.title=Compara +home.compare.desc=Compara i mostra les diferències entre 2 documents PDF + +downloadPdf=Descarregueu PDF +text=Text +font=Tipus de lletra + +removeBlanks.title=Elimina els espais en blanc +removeBlanks.header=Elimina les pàgines en blanc +removeBlanks.threshold=Llindar: +removeBlanks.thresholdDesc=Llindar per determinar el blanc que ha de ser un píxel blanc +removeBlanks.whitePercent=Percentatge blanc (%): +removeBlanks.whitePercentDesc=Percentatge de pàgina que ha de ser blanca per eliminar-la +removeBlanks.submit=Elimina els espais en blanc + +compare.title=Comparar +compare.header=Compara PDF +compare.document.1=Document 1 +compare.document.2=Document 2 +compare.submit=Comparar + +sign.title=Sign +sign.header=Firma els PDF +sign.upload=Penja la imatge +sign.draw=Dibuixa la signatura +sign.text=Entrada de text +sign.clear=Esborrar +sign.add=Afegeix + +repair.title=Reparar +repair.header=Repara els PDF +repair.submit=Reparar + +flatten.title=Aplanar +flatten.header=Aplana els PDF +flatten.submit=Aplanar ScannerImageSplit.selectText.1=Llindar d'angle: ScannerImageSplit.selectText.2=Estableix l'angle absolut mínim necessari perquè la imatge es giri (per defecte: 10). diff --git a/src/main/resources/messages_de_DE.properties b/src/main/resources/messages_de_DE.properties index 57430b55..ea8df7ab 100644 --- a/src/main/resources/messages_de_DE.properties +++ b/src/main/resources/messages_de_DE.properties @@ -107,6 +107,55 @@ home.PDFToXML.desc=PDF in XML-Format konvertieren home.ScannerImageSplit.title=Gescannte Fotos erkennen/aufteilen home.ScannerImageSplit.desc=Teilt mehrere Fotos innerhalb eines Fotos/PDF +home.sign.title=Signieren +home.sign.desc=Fügt PDF-Signaturen durch Zeichnung, Text oder Bild hinzu + +home.flatten.title=Abflachen +home.flatten.desc=Alle interaktiven Elemente und Formulare aus einem PDF entfernen + +home.repair.title=Reparatur +home.repair.desc=Versucht, ein beschädigtes/kaputtes PDF zu reparieren + +home.removeBlanks.title=Leere Seiten entfernen +home.removeBlanks.desc=Erkennt und entfernt leere Seiten aus einem Dokument + +home.compare.title=Vergleichen +home.compare.desc=Vergleicht und zeigt die Unterschiede zwischen zwei PDF-Dokumenten an + +downloadPdf=PDF herunterladen +text=Text +font=Schriftart + +removeBlanks.title=Leerzeichen entfernen +removeBlanks.header=Leere Seiten entfernen +removeBlanks.threshold=Schwellenwert: +removeBlanks.thresholdDesc=Schwellenwert zur Bestimmung, wie weiß ein weißer Pixel sein muss +removeBlanks.whitePercent=Weißprozentsatz (%): +removeBlanks.whitePercentDesc=Prozentsatz der Seite, die weiß sein muss, um entfernt zu werden +removeBlanks.submit=Leerzeichen entfernen + +compare.title=Vergleichen +compare.header=PDFs vergleichen +compare.document.1=Dokument 1 +compare.document.2=Dokument 2 +compare.submit=Vergleichen + +sign.title=Signieren +sign.header=PDFs signieren +sign.upload=Bild hochladen +sign.draw=Signatur zeichnen +sign.text=Texteingabe +sign.clear=Klar +sign.add=Hinzufügen + +repair.title=Reparieren +Repair.header=PDFs reparieren +repair.submit=Reparieren + +flatten.title=Abflachen +flatten.header=PDFs reduzieren +flatten.submit=Abflachen + ScannerImageSplit.selectText.1=Winkelschwelle: ScannerImageSplit.selectText.2=Legt den minimalen absoluten Winkel fest, der erforderlich ist, damit das Bild gedreht werden kann (Standard: 10). ScannerImageSplit.selectText.3=Toleranz: @@ -168,7 +217,7 @@ fileToPDF.submit=In PDF konvertieren #Add image addImage.title=Bild hinzufügen -addImage.header=Ein Bild einfügen (Work in progress) +addImage.header=Ein Bild einfügen addImage.everyPage=Jede Seite? addImage.submit=Bild hinzufügen diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 6ac9683c..52bd0c56 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -51,7 +51,7 @@ home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF) home.pdfOrganiser.title=Organise home.pdfOrganiser.desc=Remove/Rearrange pages in any order -home.addImage.title=Add image onto PDF +home.addImage.title=Add image home.addImage.desc=Adds a image onto a set location on the PDF (Work in progress) home.watermark.title=Add Watermark @@ -81,7 +81,7 @@ home.changeMetadata.desc=Change/Remove/Add metadata from a PDF document home.fileToPDF.title=Convert file to PDF home.fileToPDF.desc=Convert nearly any file to PDF (DOCX, PNG, XLS, PPT, TXT and more) -home.ocr.title=Run OCR and/or Cleanup scans +home.ocr.title=OCR / Cleanup scans home.ocr.desc=Cleanup scans and detects text from images within a PDF and re-adds it as text. home.extractImages.title=Extract Images @@ -108,7 +108,54 @@ home.PDFToXML.desc=Convert PDF to XML format home.ScannerImageSplit.title=Detect/Split Scanned photos home.ScannerImageSplit.desc=Splits multiple photos from within a photo/PDF +home.sign.title=Sign +home.sign.desc=Adds signature to PDF by drawing, text or image +home.flatten.title=Flatten +home.flatten.desc=Remove all interactive elements and forms from a PDF + +home.repair.title=Repair +home.repair.desc=Tries to repair a corrupt/broken PDF + +home.removeBlanks.title=Remove Blank pages +home.removeBlanks.desc=Detects and removes blank pages from a document + +home.compare.title=Compare +home.compare.desc=Compares and shows the differences between 2 PDF Documents + +downloadPdf=Download PDF +text=Text +font=Font + +removeBlanks.title=Remove Blanks +removeBlanks.header=Remove Blank Pages +removeBlanks.threshold=Threshold: +removeBlanks.thresholdDesc=Threshold for determining how white a white pixel must be +removeBlanks.whitePercent=White Percent (%): +removeBlanks.whitePercentDesc=Percent of page that must be white to be removed +removeBlanks.submit=Remove Blanks + +compare.title=Compare +compare.header=Compare PDFs +compare.document.1=Document 1 +compare.document.2=Document 2 +compare.submit=Compare + +sign.title=Sign +sign.header=Sign PDFs +sign.upload=Upload Image +sign.draw=Draw Signature +sign.text=Text Input +sign.clear=Clear +sign.add=Add + +repair.title=Repair +repair.header=Repair PDFs +repair.submit=Repair + +flatten.title=Flatten +flatten.header=Flatten PDFs +flatten.submit=Flatten ScannerImageSplit.selectText.1=Angle Threshold: ScannerImageSplit.selectText.2=Sets the minimum absolute angle required for the image to be rotated (default: 10). @@ -186,7 +233,7 @@ compress.submit=Compress #Add image addImage.title=Add Image -addImage.header=Add image to PDF (Work in progress) +addImage.header=Add image to PDF addImage.everyPage=Every Page? addImage.submit=Add image diff --git a/src/main/resources/messages_es_ES.properties b/src/main/resources/messages_es_ES.properties index a8712edb..eb564511 100644 --- a/src/main/resources/messages_es_ES.properties +++ b/src/main/resources/messages_es_ES.properties @@ -107,6 +107,55 @@ home.PDFToXML.desc=Convertir PDF a formato XML home.ScannerImageSplit.title=Detectar/Dividir fotos escaneadas home.ScannerImageSplit.desc=Dividir varias fotos dentro de una foto/PDF +home.sign.title=Firmar +home.sign.desc=Añade firma a PDF mediante dibujo, texto o imagen + +home.flatten.title=Aplanar +home.flatten.desc=Eliminar todos los elementos y formularios interactivos de un PDF + +home.repair.title=Reparar +home.repair.desc=Intenta reparar un PDF corrupto/roto + +home.removeBlanks.title=Eliminar páginas en blanco +home.removeBlanks.desc=Detecta y elimina páginas en blanco de un documento + +home.compare.title=Comparar +home.compare.desc=Compara y muestra las diferencias entre 2 documentos PDF + +downloadPdf=Descargar PDF +text=Texto +font=Fuente + +removeBlanks.title=Eliminar espacios en blanco +removeBlanks.header=Eliminar páginas en blanco +removeBlanks.threshold=Umbral: +removeBlanks.thresholdDesc=Umbral para determinar qué tan blanco debe ser un píxel blanco +removeBlanks.whitePercent=Porcentaje de blanco (%): +removeBlanks.whitePercentDesc=Porcentaje de página que debe ser blanca para ser eliminada +removeBlanks.submit=Eliminar espacios en blanco + +compare.title=Comparar +compare.header=Comparar archivos PDF +compare.document.1=Documento 1 +compare.document.2=Documento 2 +compare.submit=Comparar + +sign.title=Firmar +sign.header=Firmar archivos PDF +sign.upload=Subir imagen +sign.draw=Dibujar firma +sign.text=Entrada de texto +sign.clear=Borrar +sign.add=Agregar + +repair.title=Reparar +repair.header=Reparar archivos PDF +repair.submit=Reparar + +flatten.title=Aplanar +flatten.header=Acoplar archivos PDF +flatten.submit=Aplanar + ScannerImageSplit.selectText.1=Umbral de ángulo: ScannerImageSplit.selectText.2=Establece el ángulo absoluto mínimo requerido para rotar la imagen (predeterminado: 10). ScannerImageSplit.selectText.3=Tolerancia: @@ -182,7 +231,7 @@ compress.submit=Comprimir #Add image addImage.title=Añade Imagen -addImage.header=Añade image de PDF (Trabajo en progreso) +addImage.header=Añade image de PDF addImage.everyPage=¿Todas las páginas? addImage.submit=Añade imagen diff --git a/src/main/resources/messages_fr_FR.properties b/src/main/resources/messages_fr_FR.properties index ddba2305..34378109 100644 --- a/src/main/resources/messages_fr_FR.properties +++ b/src/main/resources/messages_fr_FR.properties @@ -113,6 +113,55 @@ home.PDFToXML.desc=Convertir le PDF au format XML home.ScannerImageSplit.title=Détecter/diviser les photos numérisées home.ScannerImageSplit.desc=Divise plusieurs photos à partir d'une photo/PDF +home.sign.title=Signe +home.sign.desc=Ajoute une signature au PDF par dessin, texte ou image + +home.flatten.title=Aplatir +home.flatten.desc=Supprimer tous les éléments et formulaires interactifs d'un PDF + +home.repair.title=Réparer +home.repair.desc=Essaye de réparer un PDF corrompu/cassé + +home.removeBlanks.title=Supprimer les pages vierges +home.removeBlanks.desc=Détecte et supprime les pages vierges d'un document + +home.compare.title=Comparer +home.compare.desc=Compare et affiche les différences entre 2 documents PDF + +downloadPdf=Télécharger le PDF +text=Texte +font=Police + +removeBlanks.title=Supprimer les blancs +removeBlanks.header=Supprimer les pages vierges +removeBlanks.threshold=Seuil : +removeBlanks.thresholdDesc=Seuil pour déterminer à quel point un pixel blanc doit être blanc +removeBlanks.whitePercent=Pourcentage blanc (%) : +removeBlanks.whitePercentDesc=Pourcentage de page qui doit être blanche pour être supprimée +removeBlanks.submit=Supprimer les blancs + +compare.title=Comparer +compare.header=Comparer des PDF +compare.document.1=Document 1 +compare.document.2=Document 2 +compare.submit=Comparer + +sign.title=Signe +sign.header=Signer des PDF +sign.upload=Télécharger l'image +sign.draw=Dessiner une signature +sign.text=Saisie de texte +sign.clear=Effacer +sign.add=Ajouter + +repair.title=Réparer +repair.header=Réparer les PDF +repair.submit=Réparer + +flatten.title=Aplatir +flatten.header=Aplatir les PDF +flatten.submit=Aplatir + ScannerImageSplit.selectText.1=Seuil d'angle : ScannerImageSplit.selectText.2=Définit l'angle absolu minimum requis pour la rotation de l'image (par défaut : 10). ScannerImageSplit.selectText.3=Tolérance : @@ -171,7 +220,7 @@ fileToPDF.submit=Convertir en PDF #Add image addImage.title=Ajouter une image -addImage.header=Ajouter une image au PDF (Travail en cours) +addImage.header=Ajouter une image au PDF addImage.everyPage=Chaque page? addImage.submit=Ajouter une image diff --git a/src/main/resources/messages_zh_CN.properties b/src/main/resources/messages_zh_CN.properties index 5ea4f057..bddd8b1c 100644 --- a/src/main/resources/messages_zh_CN.properties +++ b/src/main/resources/messages_zh_CN.properties @@ -52,7 +52,7 @@ home.pdfOrganiser.title=整理 home.pdfOrganiser.desc=按任何顺序删除/重新排列页面。 home.addImage.title=在PDF中添加图片 -home.addImage.desc=将图像添加到PDF的设定位置上(正在完成) +home.addImage.desc=将图像添加到PDF的设定位置上 home.watermark.title=添加水印 home.watermark.desc=在PDF中添加一个自定义的水印。 @@ -108,7 +108,54 @@ home.PDFToXML.desc=将PDF转换为XML格式 home.ScannerImageSplit.title=检测/分割扫描的照片 home.ScannerImageSplit.desc=从一张照片/PDF中分割出多张照片 +home.sign.title=\u6807\u5FD7 +home.sign.desc=\u901A\u8FC7\u7ED8\u56FE\u3001\u6587\u672C\u6216\u56FE\u50CF\u5411 PDF \u6DFB\u52A0\u7B7E\u540D +home.flatten.title=\u5C55\u5E73 +home.flatten.desc=\u4ECE PDF \u4E2D\u5220\u9664\u6240\u6709\u4EA4\u4E92\u5143\u7D20\u548C\u8868\u5355 + +home.repair.title=\u4FEE\u590D +home.repair.desc=\u5C1D\u8BD5\u4FEE\u590D\u635F\u574F/\u635F\u574F\u7684 PDF + +home.removeBlanks.title=\u5220\u9664\u7A7A\u767D\u9875 +home.removeBlanks.desc=\u68C0\u6D4B\u5E76\u5220\u9664\u6587\u6863\u4E2D\u7684\u7A7A\u767D\u9875 + +home.compare.title=\u6BD4\u8F83 +home.compare.desc=\u6BD4\u8F83\u5E76\u663E\u793A 2 \u4E2A PDF \u6587\u6863\u4E4B\u95F4\u7684\u5DEE\u5F02 + +downloadPdf=\u4E0B\u8F7DPDF +text=\u6587\u672C +font=\u5B57\u4F53 + +removeBlanks.title=\u5220\u9664\u7A7A\u767D +removeBlanks.header=\u5220\u9664\u7A7A\u767D\u9875 +removeBlanks.threshold=\u9608\u503C\uFF1A +removeBlanks.thresholdDesc=\u786E\u5B9A\u767D\u8272\u50CF\u7D20\u5FC5\u987B\u6709\u591A\u767D\u7684\u9608\u503C +removeBlanks.whitePercent=\u767D\u8272\u767E\u5206\u6BD4\uFF08%\uFF09\uFF1A +removeBlanks.whitePercentDesc=\u5FC5\u987B\u4E3A\u767D\u8272\u624D\u80FD\u5220\u9664\u7684\u9875\u9762\u767E\u5206\u6BD4 +removeBlanks.submit=\u5220\u9664\u7A7A\u767D + +compare.title=\u6BD4\u8F83 +compare.header=\u6BD4\u8F83 PDF +compare.document.1=\u6587\u6863 1 +compare.document.2=\u6587\u6863 2 +compare.submit=\u6BD4\u8F83 + +sign.title=\u7B7E\u540D +sign.header=\u7B7E\u7F72 PDF +sign.upload=\u4E0A\u4F20\u56FE\u7247 +sign.draw=\u7ED8\u5236\u7B7E\u540D +sign.text=\u6587\u672C\u8F93\u5165 +sign.clear=\u6E05\u9664 +sign.add=\u6DFB\u52A0 + +repair.title=\u4FEE\u590D +repair.header=\u4FEE\u590D PDF +repair.submit=\u4FEE\u590D + +flatten.title=\u5C55\u5E73 +flatten.header=\u5C55\u5E73 PDF +flatten.submit=\u5C55\u5E73 ScannerImageSplit.selectText.1=角度阈值: ScannerImageSplit.selectText.2=设置图像被旋转所需的最小绝对角度(默认:10)。 diff --git a/src/main/resources/static/css/dark-mode.css b/src/main/resources/static/css/dark-mode.css index 78df6386..3cc945db 100644 --- a/src/main/resources/static/css/dark-mode.css +++ b/src/main/resources/static/css/dark-mode.css @@ -1,34 +1,39 @@ /* Dark Mode Styles */ body { - background-color: #333 !important; - color: #fff !important; + --body-background-color: 51, 51, 51; + --base-font-color: 255, 255, 255; + background-color: rgb(var(--body-background-color)) !important; + color: rgb(var(--base-font-color)) !important; } .dark-card { - background-color: #333 !important; - color: white !important; + background-color: rgb(var(--body-background-color)) !important; + color: rgb(var(--base-font-color)) !important; } .jumbotron { background-color: #222; /* or any other dark color */ - color: #fff !important; /* or any other light color */ + color: rgb(var(--base-font-color)) !important; /* or any other light color */ } .list-group { background-color: #222 !important; - color: fff !important; + color: rgb(var(--base-font-color)) !important; } .list-group-item { background-color: #222 !important; - color: fff !important; + color: rgb(var(--base-font-color)) !important; } #support-section { background-color: #444 !important; } - #pages-container-wrapper { --background-color: rgba(255, 255, 255, 0.046) !important; --scroll-bar-color: #4c4c4c !important; --scroll-bar-thumb: #d3d3d3 !important; - --scroll-bar-thumb-hover: #ffffff !important; + --scroll-bar-thumb-hover: rgb(var(--base-font-color)) !important; +} + +.favorite-icon img { + filter: brightness(0) invert(1) !important; } \ No newline at end of file diff --git a/src/main/resources/static/css/general.css b/src/main/resources/static/css/general.css index 3190465e..90d08d12 100644 --- a/src/main/resources/static/css/general.css +++ b/src/main/resources/static/css/general.css @@ -16,11 +16,14 @@ html[lang-direction=ltr] * { direction: ltr; } - html[lang-direction=rtl] * { direction: rtl; text-align: right; } +.ignore-rtl { + direction: ltr !important; + text-align: left !important; +} .align-top { position: absolute; @@ -42,3 +45,16 @@ html[lang-direction=rtl] * { position: absolute; bottom: 0; } + +.btn-group > label:first-of-type { + border-top-left-radius: 0.25rem !important; + border-bottom-left-radius: 0.25rem !important; +} + +.margin-auto-parent { + width: 100%; + display: flex; +} +.margin-center { + margin: 0 auto; +} \ No newline at end of file diff --git a/src/main/resources/static/css/light-mode.css b/src/main/resources/static/css/light-mode.css new file mode 100644 index 00000000..b696c036 --- /dev/null +++ b/src/main/resources/static/css/light-mode.css @@ -0,0 +1,5 @@ +/* Dark Mode Styles */ +body { + --body-background-color: 255, 255, 255; + --base-font-color: 33, 37, 41; +} \ No newline at end of file diff --git a/src/main/resources/static/css/rainbow-mode.css b/src/main/resources/static/css/rainbow-mode.css index bd82dba1..4780931a 100644 --- a/src/main/resources/static/css/rainbow-mode.css +++ b/src/main/resources/static/css/rainbow-mode.css @@ -2,6 +2,8 @@ body { background: linear-gradient(90deg, rgba(255,0,0,1) 0%, rgba(255,154,0,1) 10%, rgba(208,222,33,1) 20%, rgba(79,220,74,1) 30%, rgba(63,218,216,1) 40%, rgba(47,201,226,1) 50%, rgba(28,127,238,1) 60%, rgba(95,21,242,1) 70%, rgba(186,12,248,1) 80%, rgba(251,7,217,1) 90%, rgba(255,0,0,1) 100%); color: #fff !important; + --body-background-color: 255, 255, 255; + --base-font-color: 33, 37, 41; } .dark-card { diff --git a/src/main/resources/static/css/tab-container.css b/src/main/resources/static/css/tab-container.css new file mode 100644 index 00000000..d1f0771b --- /dev/null +++ b/src/main/resources/static/css/tab-container.css @@ -0,0 +1,26 @@ + +.tab-group { + +} + +.tab-container { + display: none; +} +.tab-container.active { + display: block; + border: 1px solid rgba(var(--base-font-color), 0.25); + padding: 15px; +} +.tab-buttons > button { + margin-bottom: -1px; + background: 0 0; + border: 1px solid transparent; + color: rgb(var(--base-font-color)); + + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} +.tab-buttons > button.active { + background-color: rgb(var(--body-background-color)); + border-color: rgba(var(--base-font-color), 0.25) rgba(var(--base-font-color), 0.25) rgb(var(--body-background-color)); +} \ No newline at end of file diff --git a/src/main/resources/static/fonts/Estonia.woff2 b/src/main/resources/static/fonts/Estonia.woff2 new file mode 100644 index 00000000..7e8b9c27 Binary files /dev/null and b/src/main/resources/static/fonts/Estonia.woff2 differ diff --git a/src/main/resources/static/fonts/Tangerine.woff2 b/src/main/resources/static/fonts/Tangerine.woff2 new file mode 100644 index 00000000..935c2160 Binary files /dev/null and b/src/main/resources/static/fonts/Tangerine.woff2 differ diff --git a/src/main/resources/static/images/blank-file.svg b/src/main/resources/static/images/blank-file.svg new file mode 100644 index 00000000..3562fb2b --- /dev/null +++ b/src/main/resources/static/images/blank-file.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/main/resources/static/images/flags/es-ct.svg b/src/main/resources/static/images/flags/es-ct.svg new file mode 100644 index 00000000..4d859114 --- /dev/null +++ b/src/main/resources/static/images/flags/es-ct.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/static/images/flatten.svg b/src/main/resources/static/images/flatten.svg new file mode 100644 index 00000000..944240cf --- /dev/null +++ b/src/main/resources/static/images/flatten.svg @@ -0,0 +1,41 @@ + + + + + + + diff --git a/src/main/resources/static/images/scales.svg b/src/main/resources/static/images/scales.svg new file mode 100644 index 00000000..61b9c381 --- /dev/null +++ b/src/main/resources/static/images/scales.svg @@ -0,0 +1,94 @@ + + + + + + + + +image/svg+xmlOpenclipartscales of justice2009-06-26T04:35:18https://openclipart.org/detail/26849/scales-of-justice-by-johnny_automaticjohnny_automaticjusticelawmeasurementscalessilhouetteweight diff --git a/src/main/resources/static/images/sign.svg b/src/main/resources/static/images/sign.svg new file mode 100644 index 00000000..013acc25 --- /dev/null +++ b/src/main/resources/static/images/sign.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/static/images/star-fill.svg b/src/main/resources/static/images/star-fill.svg new file mode 100644 index 00000000..de09c4aa --- /dev/null +++ b/src/main/resources/static/images/star-fill.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/main/resources/static/images/star.svg b/src/main/resources/static/images/star.svg new file mode 100644 index 00000000..742b5e25 --- /dev/null +++ b/src/main/resources/static/images/star.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/main/resources/static/images/wrench.svg b/src/main/resources/static/images/wrench.svg new file mode 100644 index 00000000..bef07136 --- /dev/null +++ b/src/main/resources/static/images/wrench.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 new file mode 100644 index 00000000..f69aaa85 --- /dev/null +++ b/src/main/resources/static/js/draggable-utils.js @@ -0,0 +1,265 @@ +const DraggableUtils = { + + boxDragContainer: document.getElementById('box-drag-container'), + pdfCanvas: document.getElementById('pdf-canvas'), + nextId: 0, + pdfDoc: null, + pageIndex: 0, + documentsMap: new Map(), + + init() { + interact('.draggable-canvas') + .draggable({ + listeners: { + move: (event) => { + const target = event.target; + const x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx; + const y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy; + + target.style.transform = `translate(${x}px, ${y}px)`; + target.setAttribute('data-x', x); + target.setAttribute('data-y', y); + + this.onInteraction(target); + }, + }, + }) + .resizable({ + edges: { left: true, right: true, bottom: true, top: true }, + listeners: { + move: (event) => { + var target = event.target + var x = (parseFloat(target.getAttribute('data-x')) || 0) + var y = (parseFloat(target.getAttribute('data-y')) || 0) + + // update the element's style + target.style.width = event.rect.width + 'px' + target.style.height = event.rect.height + 'px' + + // translate when resizing from top or left edges + x += event.deltaRect.left + y += event.deltaRect.top + + target.style.transform = 'translate(' + x + 'px,' + y + 'px)' + + target.setAttribute('data-x', x) + target.setAttribute('data-y', y) + target.textContent = Math.round(event.rect.width) + '\u00D7' + Math.round(event.rect.height) + + this.onInteraction(target); + }, + }, + modifiers: [ + interact.modifiers.restrictSize({ + min: { width: 50, height: 50 }, + }), + ], + inertia: true, + }); + }, + onInteraction(target) { + this.boxDragContainer.appendChild(target); + }, + + createDraggableCanvas() { + const createdCanvas = document.createElement('canvas'); + createdCanvas.id = `draggable-canvas-${this.nextId++}`; + createdCanvas.classList.add("draggable-canvas"); + + const x = 0; + const y = 20; + createdCanvas.style.transform = `translate(${x}px, ${y}px)`; + createdCanvas.setAttribute('data-x', x); + createdCanvas.setAttribute('data-y', y); + + createdCanvas.onclick = e => this.onInteraction(e.target); + + this.boxDragContainer.appendChild(createdCanvas); + return createdCanvas; + }, + createDraggableCanvasFromUrl(dataUrl) { + return new Promise((resolve) => { + var myImage = new Image(); + myImage.src = dataUrl; + myImage.onload = () => { + var createdCanvas = this.createDraggableCanvas(); + + createdCanvas.width = myImage.width; + createdCanvas.height = myImage.height; + + const imgAspect = myImage.width / myImage.height; + const pdfAspect = this.boxDragContainer.offsetWidth / this.boxDragContainer.offsetHeight; + + var scaleMultiplier; + if (imgAspect > pdfAspect) { + scaleMultiplier = this.boxDragContainer.offsetWidth / myImage.width; + } else { + scaleMultiplier = this.boxDragContainer.offsetHeight / myImage.height; + } + + var newWidth = createdCanvas.width; + var newHeight = createdCanvas.height; + if (scaleMultiplier < 1) { + newWidth = newWidth * scaleMultiplier; + newHeight = newHeight * scaleMultiplier; + } + + createdCanvas.style.width = newWidth+"px"; + createdCanvas.style.height = newHeight+"px"; + + var myContext = createdCanvas.getContext("2d"); + myContext.drawImage(myImage,0,0); + resolve(createdCanvas); + } + }) + }, + deleteAllDraggableCanvases() { + this.boxDragContainer.querySelectorAll(".draggable-canvas").forEach(el => el.remove()); + }, + deleteDraggableCanvas(element) { + if (element) { + element.remove(); + } + }, + getLastInteracted() { + return this.boxDragContainer.querySelector(".draggable-canvas:last-of-type"); + }, + + storePageContents() { + var pagesMap = this.documentsMap.get(this.pdfDoc); + if (!pagesMap) { + pagesMap = {}; + } + + const elements = [...this.boxDragContainer.querySelectorAll(".draggable-canvas")]; + const draggablesData = elements.map(el => {return{element:el, offsetWidth:el.offsetWidth, offsetHeight:el.offsetHeight}}); + elements.forEach(el => this.boxDragContainer.removeChild(el)); + + pagesMap[this.pageIndex] = draggablesData; + pagesMap[this.pageIndex+"-offsetWidth"] = this.pdfCanvas.offsetWidth; + pagesMap[this.pageIndex+"-offsetHeight"] = this.pdfCanvas.offsetHeight; + + this.documentsMap.set(this.pdfDoc, pagesMap); + }, + loadPageContents() { + var pagesMap = this.documentsMap.get(this.pdfDoc); + this.deleteAllDraggableCanvases(); + if (!pagesMap) { + return; + } + + const draggablesData = pagesMap[this.pageIndex]; + if (draggablesData) { + draggablesData.forEach(draggableData => this.boxDragContainer.appendChild(draggableData.element)); + } + + this.documentsMap.set(this.pdfDoc, pagesMap); + }, + + async renderPage(pdfDocument, pageIdx) { + this.pdfDoc = pdfDocument ? pdfDocument : this.pdfDoc; + this.pageIndex = pageIdx; + + // persist + const page = await this.pdfDoc.getPage(this.pageIndex+1); + + // set the canvas size to the size of the page + if (page.rotate == 90 || page.rotate == 270) { + this.pdfCanvas.width = page.view[3]; + this.pdfCanvas.height = page.view[2]; + } else { + this.pdfCanvas.width = page.view[2]; + this.pdfCanvas.height = page.view[3]; + } + + // render the page onto the canvas + var renderContext = { + canvasContext: this.pdfCanvas.getContext("2d"), + viewport: page.getViewport({ scale: 1 }) + }; + await page.render(renderContext).promise; + + //return pdfCanvas.toDataURL(); + }, + async incrementPage() { + if (this.pageIndex < this.pdfDoc.numPages-1) { + this.storePageContents(); + await this.renderPage(this.pdfDoc, this.pageIndex+1); + this.loadPageContents(); + } + }, + async decrementPage() { + if (this.pageIndex > 0) { + this.storePageContents(); + await this.renderPage(this.pdfDoc, this.pageIndex-1); + this.loadPageContents(); + } + }, + + parseTransform(element) { + + }, + async getOverlayedPdfDocument() { + const pdfBytes = await this.pdfDoc.getData(); + const pdfDocModified = await PDFLib.PDFDocument.load(pdfBytes); + this.storePageContents(); + + const pagesMap = this.documentsMap.get(this.pdfDoc); + for (let pageIdx in pagesMap) { + if (pageIdx.includes("offset")) { + continue; + } + console.log(typeof pageIdx); + + const page = pdfDocModified.getPage(parseInt(pageIdx)); + const draggablesData = pagesMap[pageIdx]; + const offsetWidth = pagesMap[pageIdx+"-offsetWidth"]; + const offsetHeight = pagesMap[pageIdx+"-offsetHeight"]; + + for (const draggableData of draggablesData) { + // embed the draggable canvas + const draggableElement = draggableData.element; + const response = await fetch(draggableElement.toDataURL()); + const draggableImgBytes = await response.arrayBuffer(); + const pdfImageObject = await pdfDocModified.embedPng(draggableImgBytes); + + // calculate the position in the pdf document + const tansform = draggableElement.style.transform.replace(/[^.,-\d]/g, ''); + const transformComponents = tansform.split(","); + const draggablePositionPixels = { + x: parseFloat(transformComponents[0]), + y: parseFloat(transformComponents[1]), + width: draggableData.offsetWidth, + height: draggableData.offsetHeight, + }; + const draggablePositionRelative = { + x: draggablePositionPixels.x / offsetWidth, + y: draggablePositionPixels.y / offsetHeight, + width: draggablePositionPixels.width / offsetWidth, + height: draggablePositionPixels.height / offsetHeight, + } + const draggablePositionPdf = { + x: draggablePositionRelative.x * page.getWidth(), + y: draggablePositionRelative.y * page.getHeight(), + width: draggablePositionRelative.width * page.getWidth(), + height: draggablePositionRelative.height * page.getHeight(), + } + + // draw the image + page.drawImage(pdfImageObject, { + x: draggablePositionPdf.x, + y: page.getHeight() - draggablePositionPdf.y - draggablePositionPdf.height, + width: draggablePositionPdf.width, + height: draggablePositionPdf.height, + }); + } + } + + this.loadPageContents(); + return pdfDocModified; + }, +} + +document.addEventListener("DOMContentLoaded", () => { + DraggableUtils.init(); +}); diff --git a/src/main/resources/static/js/game.js b/src/main/resources/static/js/game.js index 2d138f93..ffad304b 100644 --- a/src/main/resources/static/js/game.js +++ b/src/main/resources/static/js/game.js @@ -108,16 +108,17 @@ function initializeGame() { } -function resetEnemies() { - pdfs.forEach((pdf) => gameContainer.removeChild(pdf)); - pdfs.length = 0; -} + function resetEnemies() { + pdfs.forEach((pdf) => gameContainer.removeChild(pdf)); + pdfs.length = 0; + } function updateGame() { if (gameOver || paused) return; - pdfs.forEach((pdf, pdfIndex) => { + for (let pdfIndex = 0; pdfIndex < pdfs.length; pdfIndex++) { + const pdf = pdfs[pdfIndex]; const pdfY = parseInt(pdf.style.top) + pdfSpeed; if (pdfY + 50 > gameContainer.clientHeight) { gameContainer.removeChild(pdf); @@ -149,11 +150,7 @@ function resetEnemies() { } } } - }); - - - - + }; projectiles.forEach((projectile, projectileIndex) => { const projectileY = parseInt(projectile.style.top) - 10; @@ -180,31 +177,32 @@ function resetEnemies() { setTimeout(updateGame, 1000 / 60); } -function resetGame() { - playerX = gameContainer.clientWidth / 2; - playerY = 50; - updatePlayerPosition(); - pdfs.forEach((pdf) => gameContainer.removeChild(pdf)); - projectiles.forEach((projectile) => gameContainer.removeChild(projectile)); + function resetGame() { + playerX = gameContainer.clientWidth / 2; + playerY = 50; + updatePlayerPosition(); - pdfs.length = 0; - projectiles.length = 0; + pdfs.forEach((pdf) => gameContainer.removeChild(pdf)); + projectiles.forEach((projectile) => gameContainer.removeChild(projectile)); - score = 0; - level = 1; - lives = 3; - - gameOver = false; + pdfs.length = 0; + projectiles.length = 0; - updateScore(); - updateLives(); - levelElement.textContent = 'Level: ' + level; - pdfSpeed = 1; - clearTimeout(spawnPdfTimeout); // Clear the existing spawnPdfTimeout - setTimeout(updateGame, 1000 / 60); - spawnPdfInterval(); -} + score = 0; + level = 1; + lives = 3; + + gameOver = false; + + updateScore(); + updateLives(); + levelElement.textContent = 'Level: ' + level; + pdfSpeed = 1; + clearTimeout(spawnPdfTimeout); // Clear the existing spawnPdfTimeout + setTimeout(updateGame, 1000 / 60); + spawnPdfInterval(); + } @@ -243,9 +241,7 @@ function resetGame() { updateHighScore(); } alert('Game Over! Your final score is: ' + score); - setTimeout(() => { // Wrap the resetGame() call in a setTimeout - resetGame(); - }, 0); + document.getElementById('game-container-wrapper').close(); } @@ -281,6 +277,7 @@ function resetGame() { }); + window.resetGame = resetGame; } window.initializeGame = initializeGame; diff --git a/src/main/resources/static/js/interact.min.js b/src/main/resources/static/js/interact.min.js new file mode 100644 index 00000000..442e0e7e --- /dev/null +++ b/src/main/resources/static/js/interact.min.js @@ -0,0 +1,3 @@ +/* interact.js 1.10.11 | https://interactjs.io/license */ +!function(t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).interact=t()}((function(){var t={};Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0,t.default=function(t){return!(!t||!t.Window)&&t instanceof t.Window};var e={};Object.defineProperty(e,"__esModule",{value:!0}),e.init=o,e.getWindow=function(e){return(0,t.default)(e)?e:(e.ownerDocument||e).defaultView||r.window},e.window=e.realWindow=void 0;var n=void 0;e.realWindow=n;var r=void 0;function o(t){e.realWindow=n=t;var o=t.document.createTextNode("");o.ownerDocument!==t.document&&"function"==typeof t.wrap&&t.wrap(o)===o&&(t=t.wrap(t)),e.window=r=t}e.window=r,"undefined"!=typeof window&&window&&o(window);var i={};function a(t){return(a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}Object.defineProperty(i,"__esModule",{value:!0}),i.default=void 0;var s=function(t){return!!t&&"object"===a(t)},l=function(t){return"function"==typeof t},u={window:function(n){return n===e.window||(0,t.default)(n)},docFrag:function(t){return s(t)&&11===t.nodeType},object:s,func:l,number:function(t){return"number"==typeof t},bool:function(t){return"boolean"==typeof t},string:function(t){return"string"==typeof t},element:function(t){if(!t||"object"!==a(t))return!1;var n=e.getWindow(t)||e.window;return/object|function/.test(a(n.Element))?t instanceof n.Element:1===t.nodeType&&"string"==typeof t.nodeName},plainObject:function(t){return s(t)&&!!t.constructor&&/function Object\b/.test(t.constructor.toString())},array:function(t){return s(t)&&void 0!==t.length&&l(t.splice)}};i.default=u;var c={};function f(t){var e=t.interaction;if("drag"===e.prepared.name){var n=e.prepared.axis;"x"===n?(e.coords.cur.page.y=e.coords.start.page.y,e.coords.cur.client.y=e.coords.start.client.y,e.coords.velocity.client.y=0,e.coords.velocity.page.y=0):"y"===n&&(e.coords.cur.page.x=e.coords.start.page.x,e.coords.cur.client.x=e.coords.start.client.x,e.coords.velocity.client.x=0,e.coords.velocity.page.x=0)}}function d(t){var e=t.iEvent,n=t.interaction;if("drag"===n.prepared.name){var r=n.prepared.axis;if("x"===r||"y"===r){var o="x"===r?"y":"x";e.page[o]=n.coords.start.page[o],e.client[o]=n.coords.start.client[o],e.delta[o]=0}}}Object.defineProperty(c,"__esModule",{value:!0}),c.default=void 0;var p={id:"actions/drag",install:function(t){var e=t.actions,n=t.Interactable,r=t.defaults;n.prototype.draggable=p.draggable,e.map.drag=p,e.methodDict.drag="draggable",r.actions.drag=p.defaults},listeners:{"interactions:before-action-move":f,"interactions:action-resume":f,"interactions:action-move":d,"auto-start:check":function(t){var e=t.interaction,n=t.interactable,r=t.buttons,o=n.options.drag;if(o&&o.enabled&&(!e.pointerIsDown||!/mouse|pointer/.test(e.pointerType)||0!=(r&n.options.drag.mouseButtons)))return t.action={name:"drag",axis:"start"===o.lockAxis?o.startAxis:o.lockAxis},!1}},draggable:function(t){return i.default.object(t)?(this.options.drag.enabled=!1!==t.enabled,this.setPerAction("drag",t),this.setOnEvents("drag",t),/^(xy|x|y|start)$/.test(t.lockAxis)&&(this.options.drag.lockAxis=t.lockAxis),/^(xy|x|y)$/.test(t.startAxis)&&(this.options.drag.startAxis=t.startAxis),this):i.default.bool(t)?(this.options.drag.enabled=t,this):this.options.drag},beforeMove:f,move:d,defaults:{startAxis:"xy",lockAxis:"xy"},getCursor:function(){return"move"}},v=p;c.default=v;var h={};Object.defineProperty(h,"__esModule",{value:!0}),h.default=void 0;var g={init:function(t){var e=t;g.document=e.document,g.DocumentFragment=e.DocumentFragment||y,g.SVGElement=e.SVGElement||y,g.SVGSVGElement=e.SVGSVGElement||y,g.SVGElementInstance=e.SVGElementInstance||y,g.Element=e.Element||y,g.HTMLElement=e.HTMLElement||g.Element,g.Event=e.Event,g.Touch=e.Touch||y,g.PointerEvent=e.PointerEvent||e.MSPointerEvent},document:null,DocumentFragment:null,SVGElement:null,SVGSVGElement:null,SVGElementInstance:null,Element:null,HTMLElement:null,Event:null,Touch:null,PointerEvent:null};function y(){}var m=g;h.default=m;var b={};Object.defineProperty(b,"__esModule",{value:!0}),b.default=void 0;var x={init:function(t){var e=h.default.Element,n=t.navigator||{};x.supportsTouch="ontouchstart"in t||i.default.func(t.DocumentTouch)&&h.default.document instanceof t.DocumentTouch,x.supportsPointerEvent=!1!==n.pointerEnabled&&!!h.default.PointerEvent,x.isIOS=/iP(hone|od|ad)/.test(n.platform),x.isIOS7=/iP(hone|od|ad)/.test(n.platform)&&/OS 7[^\d]/.test(n.appVersion),x.isIe9=/MSIE 9/.test(n.userAgent),x.isOperaMobile="Opera"===n.appName&&x.supportsTouch&&/Presto/.test(n.userAgent),x.prefixedMatchesSelector="matches"in e.prototype?"matches":"webkitMatchesSelector"in e.prototype?"webkitMatchesSelector":"mozMatchesSelector"in e.prototype?"mozMatchesSelector":"oMatchesSelector"in e.prototype?"oMatchesSelector":"msMatchesSelector",x.pEventTypes=x.supportsPointerEvent?h.default.PointerEvent===t.MSPointerEvent?{up:"MSPointerUp",down:"MSPointerDown",over:"mouseover",out:"mouseout",move:"MSPointerMove",cancel:"MSPointerCancel"}:{up:"pointerup",down:"pointerdown",over:"pointerover",out:"pointerout",move:"pointermove",cancel:"pointercancel"}:null,x.wheelEvent=h.default.document&&"onmousewheel"in h.default.document?"mousewheel":"wheel"},supportsTouch:null,supportsPointerEvent:null,isIOS7:null,isIOS:null,isIe9:null,isOperaMobile:null,prefixedMatchesSelector:null,pEventTypes:null,wheelEvent:null},w=x;b.default=w;var _={};function P(t){var e=t.parentNode;if(i.default.docFrag(e)){for(;(e=e.host)&&i.default.docFrag(e););return e}return e}function O(t,n){return e.window!==e.realWindow&&(n=n.replace(/\/deep\//g," ")),t[b.default.prefixedMatchesSelector](n)}Object.defineProperty(_,"__esModule",{value:!0}),_.nodeContains=function(t,e){if(t.contains)return t.contains(e);for(;e;){if(e===t)return!0;e=e.parentNode}return!1},_.closest=function(t,e){for(;i.default.element(t);){if(O(t,e))return t;t=P(t)}return null},_.parentNode=P,_.matchesSelector=O,_.indexOfDeepestElement=function(t){for(var n,r=[],o=0;o=(parseInt(e.getWindow(g).getComputedStyle(g).zIndex,10)||0)&&(n=o);else n=o}else n=o}var v,g;return n},_.matchesUpTo=function(t,e,n){for(;i.default.element(t);){if(O(t,e))return!0;if((t=P(t))===n)return O(t,e)}return!1},_.getActualElement=function(t){return t.correspondingUseElement||t},_.getScrollXY=T,_.getElementClientRect=M,_.getElementRect=function(t){var n=M(t);if(!b.default.isIOS7&&n){var r=T(e.getWindow(t));n.left+=r.x,n.right+=r.x,n.top+=r.y,n.bottom+=r.y}return n},_.getPath=function(t){for(var e=[];t;)e.push(t),t=P(t);return e},_.trySelector=function(t){return!!i.default.string(t)&&(h.default.document.querySelector(t),!0)};var S=function(t){return t.parentNode||t.host};function E(t,e){for(var n,r=[],o=t;(n=S(o))&&o!==e&&n!==o.ownerDocument;)r.unshift(o),o=n;return r}function T(t){return{x:(t=t||e.window).scrollX||t.document.documentElement.scrollLeft,y:t.scrollY||t.document.documentElement.scrollTop}}function M(t){var e=t instanceof h.default.SVGElement?t.getBoundingClientRect():t.getClientRects()[0];return e&&{left:e.left,right:e.right,top:e.top,bottom:e.bottom,width:e.width||e.right-e.left,height:e.height||e.bottom-e.top}}var j={};Object.defineProperty(j,"__esModule",{value:!0}),j.default=function(t,e){for(var n in e)t[n]=e[n];return t};var k={};function I(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,r=Array(e);n1?q(e):e[0];U(r,t.page),V(r,t.client),t.timeStamp=n},B.getTouchPair=N,B.pointerAverage=q,B.touchBBox=function(t){if(!t.length)return null;var e=N(t),n=Math.min(e[0].pageX,e[1].pageX),r=Math.min(e[0].pageY,e[1].pageY),o=Math.max(e[0].pageX,e[1].pageX),i=Math.max(e[0].pageY,e[1].pageY);return{x:n,y:r,left:n,top:r,right:o,bottom:i,width:o-n,height:i-r}},B.touchDistance=function(t,e){var n=e+"X",r=e+"Y",o=N(t),i=o[0][n]-o[1][n],a=o[0][r]-o[1][r];return(0,C.default)(i,a)},B.touchAngle=function(t,e){var n=e+"X",r=e+"Y",o=N(t),i=o[1][n]-o[0][n],a=o[1][r]-o[0][r];return 180*Math.atan2(a,i)/Math.PI},B.getPointerType=function(t){return i.default.string(t.pointerType)?t.pointerType:i.default.number(t.pointerType)?[void 0,void 0,"touch","pen","mouse"][t.pointerType]:/touch/.test(t.type||"")||t instanceof h.default.Touch?"touch":"mouse"},B.getEventTargets=function(t){var e=i.default.func(t.composedPath)?t.composedPath():t.path;return[_.getActualElement(e?e[0]:t.target),_.getActualElement(t.currentTarget)]},B.newCoords=function(){return{page:{x:0,y:0},client:{x:0,y:0},timeStamp:0}},B.coordsToEvent=function(t){return{coords:t,get page(){return this.coords.page},get client(){return this.coords.client},get timeStamp(){return this.coords.timeStamp},get pageX(){return this.coords.page.x},get pageY(){return this.coords.page.y},get clientX(){return this.coords.client.x},get clientY(){return this.coords.client.y},get pointerId(){return this.coords.pointerId},get target(){return this.coords.target},get type(){return this.coords.type},get pointerType(){return this.coords.pointerType},get buttons(){return this.coords.buttons},preventDefault:function(){}}},Object.defineProperty(B,"pointerExtend",{enumerable:!0,get:function(){return F.default}});var $={};function G(t,e){for(var n=0;ns.left&&f.xs.top&&f.y=s.left&&h<=s.right&&g>=s.top&&g<=s.bottom}return v&&i.default.number(u)&&(l=Math.max(0,Math.min(s.right,v.right)-Math.max(s.left,v.left))*Math.max(0,Math.min(s.bottom,v.bottom)-Math.max(s.top,v.top))/(v.width*v.height)>=u),t.options.drop.checker&&(l=t.options.drop.checker(e,n,l,t,a,r,o)),l}(this,t,e,n,r,o,a)},n.dynamicDrop=function(e){return i.default.bool(e)?(t.dynamicDrop=e,n):t.dynamicDrop},(0,j.default)(e.phaselessTypes,{dragenter:!0,dragleave:!0,dropactivate:!0,dropdeactivate:!0,dropmove:!0,drop:!0}),e.methodDict.drop="dropzone",t.dynamicDrop=!1,o.actions.drop=gt.defaults},listeners:{"interactions:before-action-start":function(t){var e=t.interaction;"drag"===e.prepared.name&&(e.dropState={cur:{dropzone:null,element:null},prev:{dropzone:null,element:null},rejected:null,events:null,activeDrops:[]})},"interactions:after-action-start":function(t,e){var n=t.interaction,r=(t.event,t.iEvent);if("drag"===n.prepared.name){var o=n.dropState;o.activeDrops=null,o.events=null,o.activeDrops=ft(e,n.element),o.events=pt(n,0,r),o.events.activate&&(ct(o.activeDrops,o.events.activate),e.fire("actions/drop:start",{interaction:n,dragEvent:r}))}},"interactions:action-move":ht,"interactions:after-action-move":function(t,e){var n=t.interaction,r=t.iEvent;"drag"===n.prepared.name&&(vt(n,n.dropState.events),e.fire("actions/drop:move",{interaction:n,dragEvent:r}),n.dropState.events={})},"interactions:action-end":function(t,e){if("drag"===t.interaction.prepared.name){var n=t.interaction,r=t.iEvent;ht(t,e),vt(n,n.dropState.events),e.fire("actions/drop:end",{interaction:n,dragEvent:r})}},"interactions:stop":function(t){var e=t.interaction;if("drag"===e.prepared.name){var n=e.dropState;n&&(n.activeDrops=null,n.events=null,n.cur.dropzone=null,n.cur.element=null,n.prev.dropzone=null,n.prev.element=null,n.rejected=!1)}}},getActiveDrops:ft,getDrop:dt,getDropEvents:pt,fireDropEvents:vt,defaults:{enabled:!1,accept:null,overlap:"pointer"}},yt=gt;ut.default=yt;var mt={};function bt(t){var e=t.interaction,n=t.iEvent,r=t.phase;if("gesture"===e.prepared.name){var o=e.pointers.map((function(t){return t.pointer})),a="start"===r,s="end"===r,l=e.interactable.options.deltaSource;if(n.touches=[o[0],o[1]],a)n.distance=B.touchDistance(o,l),n.box=B.touchBBox(o),n.scale=1,n.ds=0,n.angle=B.touchAngle(o,l),n.da=0,e.gesture.startDistance=n.distance,e.gesture.startAngle=n.angle;else if(s){var u=e.prevEvent;n.distance=u.distance,n.box=u.box,n.scale=u.scale,n.ds=0,n.angle=u.angle,n.da=0}else n.distance=B.touchDistance(o,l),n.box=B.touchBBox(o),n.scale=n.distance/e.gesture.startDistance,n.angle=B.touchAngle(o,l),n.ds=n.scale-e.gesture.scale,n.da=n.angle-e.gesture.angle;e.gesture.distance=n.distance,e.gesture.angle=n.angle,i.default.number(n.scale)&&n.scale!==1/0&&!isNaN(n.scale)&&(e.gesture.scale=n.scale)}}Object.defineProperty(mt,"__esModule",{value:!0}),mt.default=void 0;var xt={id:"actions/gesture",before:["actions/drag","actions/resize"],install:function(t){var e=t.actions,n=t.Interactable,r=t.defaults;n.prototype.gesturable=function(t){return i.default.object(t)?(this.options.gesture.enabled=!1!==t.enabled,this.setPerAction("gesture",t),this.setOnEvents("gesture",t),this):i.default.bool(t)?(this.options.gesture.enabled=t,this):this.options.gesture},e.map.gesture=xt,e.methodDict.gesture="gesturable",r.actions.gesture=xt.defaults},listeners:{"interactions:action-start":bt,"interactions:action-move":bt,"interactions:action-end":bt,"interactions:new":function(t){t.interaction.gesture={angle:0,distance:0,scale:1,startAngle:0,startDistance:0}},"auto-start:check":function(t){if(!(t.interaction.pointers.length<2)){var e=t.interactable.options.gesture;if(e&&e.enabled)return t.action={name:"gesture"},!1}}},defaults:{},getCursor:function(){return""}},wt=xt;mt.default=wt;var _t={};function Pt(t,e,n,r,o,a,s){if(!e)return!1;if(!0===e){var l=i.default.number(a.width)?a.width:a.right-a.left,u=i.default.number(a.height)?a.height:a.bottom-a.top;if(s=Math.min(s,Math.abs(("left"===t||"right"===t?l:u)/2)),l<0&&("left"===t?t="right":"right"===t&&(t="left")),u<0&&("top"===t?t="bottom":"bottom"===t&&(t="top")),"left"===t)return n.x<(l>=0?a.left:a.right)+s;if("top"===t)return n.y<(u>=0?a.top:a.bottom)+s;if("right"===t)return n.x>(l>=0?a.right:a.left)-s;if("bottom"===t)return n.y>(u>=0?a.bottom:a.top)-s}return!!i.default.element(r)&&(i.default.element(e)?e===r:_.matchesUpTo(r,e,o))}function Ot(t){var e=t.iEvent,n=t.interaction;if("resize"===n.prepared.name&&n.resizeAxes){var r=e;n.interactable.options.resize.square?("y"===n.resizeAxes?r.delta.x=r.delta.y:r.delta.y=r.delta.x,r.axes="xy"):(r.axes=n.resizeAxes,"x"===n.resizeAxes?r.delta.y=0:"y"===n.resizeAxes&&(r.delta.x=0))}}Object.defineProperty(_t,"__esModule",{value:!0}),_t.default=void 0;var St={id:"actions/resize",before:["actions/drag"],install:function(t){var e=t.actions,n=t.browser,r=t.Interactable,o=t.defaults;St.cursors=function(t){return t.isIe9?{x:"e-resize",y:"s-resize",xy:"se-resize",top:"n-resize",left:"w-resize",bottom:"s-resize",right:"e-resize",topleft:"se-resize",bottomright:"se-resize",topright:"ne-resize",bottomleft:"ne-resize"}:{x:"ew-resize",y:"ns-resize",xy:"nwse-resize",top:"ns-resize",left:"ew-resize",bottom:"ns-resize",right:"ew-resize",topleft:"nwse-resize",bottomright:"nwse-resize",topright:"nesw-resize",bottomleft:"nesw-resize"}}(n),St.defaultMargin=n.supportsTouch||n.supportsPointerEvent?20:10,r.prototype.resizable=function(e){return function(t,e,n){return i.default.object(e)?(t.options.resize.enabled=!1!==e.enabled,t.setPerAction("resize",e),t.setOnEvents("resize",e),i.default.string(e.axis)&&/^x$|^y$|^xy$/.test(e.axis)?t.options.resize.axis=e.axis:null===e.axis&&(t.options.resize.axis=n.defaults.actions.resize.axis),i.default.bool(e.preserveAspectRatio)?t.options.resize.preserveAspectRatio=e.preserveAspectRatio:i.default.bool(e.square)&&(t.options.resize.square=e.square),t):i.default.bool(e)?(t.options.resize.enabled=e,t):t.options.resize}(this,e,t)},e.map.resize=St,e.methodDict.resize="resizable",o.actions.resize=St.defaults},listeners:{"interactions:new":function(t){t.interaction.resizeAxes="xy"},"interactions:action-start":function(t){!function(t){var e=t.iEvent,n=t.interaction;if("resize"===n.prepared.name&&n.prepared.edges){var r=e,o=n.rect;n._rects={start:(0,j.default)({},o),corrected:(0,j.default)({},o),previous:(0,j.default)({},o),delta:{left:0,right:0,width:0,top:0,bottom:0,height:0}},r.edges=n.prepared.edges,r.rect=n._rects.corrected,r.deltaRect=n._rects.delta}}(t),Ot(t)},"interactions:action-move":function(t){!function(t){var e=t.iEvent,n=t.interaction;if("resize"===n.prepared.name&&n.prepared.edges){var r=e,o=n.interactable.options.resize.invert,i="reposition"===o||"negate"===o,a=n.rect,s=n._rects,l=s.start,u=s.corrected,c=s.delta,f=s.previous;if((0,j.default)(f,u),i){if((0,j.default)(u,a),"reposition"===o){if(u.top>u.bottom){var d=u.top;u.top=u.bottom,u.bottom=d}if(u.left>u.right){var p=u.left;u.left=u.right,u.right=p}}}else u.top=Math.min(a.top,l.bottom),u.bottom=Math.max(a.bottom,l.top),u.left=Math.min(a.left,l.right),u.right=Math.max(a.right,l.left);for(var v in u.width=u.right-u.left,u.height=u.bottom-u.top,u)c[v]=u[v]-f[v];r.edges=n.prepared.edges,r.rect=u,r.deltaRect=c}}(t),Ot(t)},"interactions:action-end":function(t){var e=t.iEvent,n=t.interaction;if("resize"===n.prepared.name&&n.prepared.edges){var r=e;r.edges=n.prepared.edges,r.rect=n._rects.corrected,r.deltaRect=n._rects.delta}},"auto-start:check":function(t){var e=t.interaction,n=t.interactable,r=t.element,o=t.rect,a=t.buttons;if(o){var s=(0,j.default)({},e.coords.cur.page),l=n.options.resize;if(l&&l.enabled&&(!e.pointerIsDown||!/mouse|pointer/.test(e.pointerType)||0!=(a&l.mouseButtons))){if(i.default.object(l.edges)){var u={left:!1,right:!1,top:!1,bottom:!1};for(var c in u)u[c]=Pt(c,l.edges[c],s,e._latestPointer.eventTarget,r,o,l.margin||St.defaultMargin);u.left=u.left&&!u.right,u.top=u.top&&!u.bottom,(u.left||u.right||u.top||u.bottom)&&(t.action={name:"resize",edges:u})}else{var f="y"!==l.axis&&s.x>o.right-St.defaultMargin,d="x"!==l.axis&&s.y>o.bottom-St.defaultMargin;(f||d)&&(t.action={name:"resize",axes:(f?"x":"")+(d?"y":"")})}return!t.action&&void 0}}}},defaults:{square:!1,preserveAspectRatio:!1,axis:"xy",margin:NaN,edges:null,invert:"none"},cursors:null,getCursor:function(t){var e=t.edges,n=t.axis,r=t.name,o=St.cursors,i=null;if(n)i=o[r+n];else if(e){for(var a="",s=["top","bottom","left","right"],l=0;l=1){var c={x:zt.x*u,y:zt.y*u};if(c.x||c.y){var f=Ft(a);i.default.window(a)?a.scrollBy(c.x,c.y):a&&(a.scrollLeft+=c.x,a.scrollTop+=c.y);var d=Ft(a),p={x:d.x-f.x,y:d.y-f.y};(p.x||p.y)&&e.fire({type:"autoscroll",target:n,interactable:e,delta:p,interaction:t,container:a})}zt.prevTime=s}zt.isScrolling&&(jt.default.cancel(zt.i),zt.i=jt.default.request(zt.scroll))},check:function(t,e){var n;return null==(n=t.options[e].autoScroll)?void 0:n.enabled},onInteractionMove:function(t){var e=t.interaction,n=t.pointer;if(e.interacting()&&zt.check(e.interactable,e.prepared.name))if(e.simulation)zt.x=zt.y=0;else{var r,o,a,s,l=e.interactable,u=e.element,c=e.prepared.name,f=l.options[c].autoScroll,d=Ct(f.container,l,u);if(i.default.window(d))s=n.clientXd.innerWidth-zt.margin,a=n.clientY>d.innerHeight-zt.margin;else{var p=_.getElementClientRect(d);s=n.clientXp.right-zt.margin,a=n.clientY>p.bottom-zt.margin}zt.x=o?1:s?-1:0,zt.y=a?1:r?-1:0,zt.isScrolling||(zt.margin=f.margin,zt.speed=f.speed,zt.start(e))}}};function Ct(t,n,r){return(i.default.string(t)?(0,k.getStringOptionResult)(t,n,r):t)||(0,e.getWindow)(r)}function Ft(t){return i.default.window(t)&&(t=window.document.body),{x:t.scrollLeft,y:t.scrollTop}}var Xt={id:"auto-scroll",install:function(t){var e=t.defaults,n=t.actions;t.autoScroll=zt,zt.now=function(){return t.now()},n.phaselessTypes.autoscroll=!0,e.perAction.autoScroll=zt.defaults},listeners:{"interactions:new":function(t){t.interaction.autoScroll=null},"interactions:destroy":function(t){t.interaction.autoScroll=null,zt.stop(),zt.interaction&&(zt.interaction=null)},"interactions:stop":zt.stop,"interactions:action-move":function(t){return zt.onInteractionMove(t)}}};Rt.default=Xt;var Yt={};Object.defineProperty(Yt,"__esModule",{value:!0}),Yt.warnOnce=function(t,n){var r=!1;return function(){return r||(e.window.console.warn(n),r=!0),t.apply(this,arguments)}},Yt.copyAction=function(t,e){return t.name=e.name,t.axis=e.axis,t.edges=e.edges,t},Yt.sign=void 0,Yt.sign=function(t){return t>=0?1:-1};var Bt={};function Wt(t){return i.default.bool(t)?(this.options.styleCursor=t,this):null===t?(delete this.options.styleCursor,this):this.options.styleCursor}function Lt(t){return i.default.func(t)?(this.options.actionChecker=t,this):null===t?(delete this.options.actionChecker,this):this.options.actionChecker}Object.defineProperty(Bt,"__esModule",{value:!0}),Bt.default=void 0;var Ut={id:"auto-start/interactableMethods",install:function(t){var e=t.Interactable;e.prototype.getAction=function(e,n,r,o){var i=function(t,e,n,r,o){var i=t.getRect(r),a={action:null,interactable:t,interaction:n,element:r,rect:i,buttons:e.buttons||{0:1,1:4,3:8,4:16}[e.button]};return o.fire("auto-start:check",a),a.action}(this,n,r,o,t);return this.options.actionChecker?this.options.actionChecker(e,n,i,this,o,r):i},e.prototype.ignoreFrom=(0,Yt.warnOnce)((function(t){return this._backCompatOption("ignoreFrom",t)}),"Interactable.ignoreFrom() has been deprecated. Use Interactble.draggable({ignoreFrom: newValue})."),e.prototype.allowFrom=(0,Yt.warnOnce)((function(t){return this._backCompatOption("allowFrom",t)}),"Interactable.allowFrom() has been deprecated. Use Interactble.draggable({allowFrom: newValue})."),e.prototype.actionChecker=Lt,e.prototype.styleCursor=Wt}};Bt.default=Ut;var Vt={};function Nt(t,e,n,r,o){return e.testIgnoreAllow(e.options[t.name],n,r)&&e.options[t.name].enabled&&Ht(e,n,t,o)?t:null}function qt(t,e,n,r,o,i,a){for(var s=0,l=r.length;s=s)return!1;if(d.interactable===t){if((u+=p===n.name?1:0)>=i)return!1;if(d.element===e&&(c++,p===n.name&&c>=a))return!1}}}return s>0}function Kt(t,e){return i.default.number(t)?(e.autoStart.maxInteractions=t,this):e.autoStart.maxInteractions}function Zt(t,e,n){var r=n.autoStart.cursorElement;r&&r!==t&&(r.style.cursor=""),t.ownerDocument.documentElement.style.cursor=e,t.style.cursor=e,n.autoStart.cursorElement=e?t:null}function Jt(t,e){var n=t.interactable,r=t.element,o=t.prepared;if("mouse"===t.pointerType&&n&&n.options.styleCursor){var a="";if(o.name){var s=n.options[o.name].cursorChecker;a=i.default.func(s)?s(o,n,r,t._interacting):e.actions.map[o.name].getCursor(o)}Zt(t.element,a||"",e)}else e.autoStart.cursorElement&&Zt(e.autoStart.cursorElement,"",e)}Object.defineProperty(Vt,"__esModule",{value:!0}),Vt.default=void 0;var Qt={id:"auto-start/base",before:["actions"],install:function(t){var e=t.interactStatic,n=t.defaults;t.usePlugin(Bt.default),n.base.actionChecker=null,n.base.styleCursor=!0,(0,j.default)(n.perAction,{manualStart:!1,max:1/0,maxPerElement:1,allowFrom:null,ignoreFrom:null,mouseButtons:1}),e.maxInteractions=function(e){return Kt(e,t)},t.autoStart={maxInteractions:1/0,withinInteractionLimit:Ht,cursorElement:null}},listeners:{"interactions:down":function(t,e){var n=t.interaction,r=t.pointer,o=t.event,i=t.eventTarget;n.interacting()||Gt(n,$t(n,r,o,i,e),e)},"interactions:move":function(t,e){!function(t,e){var n=t.interaction,r=t.pointer,o=t.event,i=t.eventTarget;"mouse"!==n.pointerType||n.pointerIsDown||n.interacting()||Gt(n,$t(n,r,o,i,e),e)}(t,e),function(t,e){var n=t.interaction;if(n.pointerIsDown&&!n.interacting()&&n.pointerWasMoved&&n.prepared.name){e.fire("autoStart:before-start",t);var r=n.interactable,o=n.prepared.name;o&&r&&(r.options[o].manualStart||!Ht(r,n.element,n.prepared,e)?n.stop():(n.start(n.prepared,r,n.element),Jt(n,e)))}}(t,e)},"interactions:stop":function(t,e){var n=t.interaction,r=n.interactable;r&&r.options.styleCursor&&Zt(n.element,"",e)}},maxInteractions:Kt,withinInteractionLimit:Ht,validateAction:Nt};Vt.default=Qt;var te={};Object.defineProperty(te,"__esModule",{value:!0}),te.default=void 0;var ee={id:"auto-start/dragAxis",listeners:{"autoStart:before-start":function(t,e){var n=t.interaction,r=t.eventTarget,o=t.dx,a=t.dy;if("drag"===n.prepared.name){var s=Math.abs(o),l=Math.abs(a),u=n.interactable.options.drag,c=u.startAxis,f=s>l?"x":s0&&(e.autoStartHoldTimer=setTimeout((function(){e.start(e.prepared,e.interactable,e.element)}),n))},"interactions:move":function(t){var e=t.interaction,n=t.duplicate;e.autoStartHoldTimer&&e.pointerWasMoved&&!n&&(clearTimeout(e.autoStartHoldTimer),e.autoStartHoldTimer=null)},"autoStart:before-start":function(t){var e=t.interaction;re(e)>0&&(e.prepared.name=null)}},getHoldDuration:re};ne.default=oe;var ie={};Object.defineProperty(ie,"__esModule",{value:!0}),ie.default=void 0;var ae={id:"auto-start",install:function(t){t.usePlugin(Vt.default),t.usePlugin(ne.default),t.usePlugin(te.default)}};ie.default=ae;var se={};function le(t){return/^(always|never|auto)$/.test(t)?(this.options.preventDefault=t,this):i.default.bool(t)?(this.options.preventDefault=t?"always":"never",this):this.options.preventDefault}function ue(t){var e=t.interaction,n=t.event;e.interactable&&e.interactable.checkAndPreventDefault(n)}function ce(t){var n=t.Interactable;n.prototype.preventDefault=le,n.prototype.checkAndPreventDefault=function(n){return function(t,n,r){var o=t.options.preventDefault;if("never"!==o)if("always"!==o){if(n.events.supportsPassive&&/^touch(start|move)$/.test(r.type)){var a=(0,e.getWindow)(r.target).document,s=n.getDocOptions(a);if(!s||!s.events||!1!==s.events.passive)return}/^(mouse|pointer|touch)*(down|start)/i.test(r.type)||i.default.element(r.target)&&(0,_.matchesSelector)(r.target,"input,select,textarea,[contenteditable=true],[contenteditable=true] *")||r.preventDefault()}else r.preventDefault()}(this,t,n)},t.interactions.docEvents.push({type:"dragstart",listener:function(e){for(var n=0;nt.length)&&(e=t.length);for(var n=0,r=Array(e);n150)return null;var e=180*Math.atan2(t.prevEvent.velocityY,t.prevEvent.velocityX)/Math.PI;e<0&&(e+=360);var n=112.5<=e&&e<247.5,r=202.5<=e&&e<337.5;return{up:r,down:!r&&22.5<=e&&e<157.5,left:n,right:!n&&(292.5<=e||e<67.5),angle:e,speed:t.prevEvent.speed,velocity:{x:t.prevEvent.velocityX,y:t.prevEvent.velocityY}}}},{key:"preventDefault",value:function(){}},{key:"stopImmediatePropagation",value:function(){this.immediatePropagationStopped=this.propagationStopped=!0}},{key:"stopPropagation",value:function(){this.propagationStopped=!0}}])&&Ie(e.prototype,n),a}($.BaseEvent);je.InteractEvent=Fe,Object.defineProperties(Fe.prototype,{pageX:{get:function(){return this.page.x},set:function(t){this.page.x=t}},pageY:{get:function(){return this.page.y},set:function(t){this.page.y=t}},clientX:{get:function(){return this.client.x},set:function(t){this.client.x=t}},clientY:{get:function(){return this.client.y},set:function(t){this.client.y=t}},dx:{get:function(){return this.delta.x},set:function(t){this.delta.x=t}},dy:{get:function(){return this.delta.y},set:function(t){this.delta.y=t}},velocityX:{get:function(){return this.velocity.x},set:function(t){this.velocity.x=t}},velocityY:{get:function(){return this.velocity.y},set:function(t){this.velocity.y=t}}});var Xe={};function Ye(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}Object.defineProperty(Xe,"__esModule",{value:!0}),Xe.PointerInfo=void 0,Xe.PointerInfo=function t(e,n,r,o,i){!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),Ye(this,"id",void 0),Ye(this,"pointer",void 0),Ye(this,"event",void 0),Ye(this,"downTime",void 0),Ye(this,"downTarget",void 0),this.id=e,this.pointer=n,this.event=r,this.downTime=o,this.downTarget=i};var Be,We,Le={};function Ue(t,e){for(var n=0;nthis.pointerMoveTolerance);var a=this.getPointerIndex(t),s={pointer:t,pointerIndex:a,pointerInfo:this.pointers[a],event:e,type:"move",eventTarget:n,dx:r,dy:o,duplicate:i,interaction:this};i||B.setCoordVelocity(this.coords.velocity,this.coords.delta),this._scopeFire("interactions:move",s),i||this.simulation||(this.interacting()&&(s.type=null,this.move(s)),this.pointerWasMoved&&B.copyCoords(this.coords.prev,this.coords.cur))}},{key:"move",value:function(t){t&&t.event||B.setZeroCoords(this.coords.delta),(t=(0,j.default)({pointer:this._latestPointer.pointer,event:this._latestPointer.event,eventTarget:this._latestPointer.eventTarget,interaction:this},t||{})).phase="move",this._doPhase(t)}},{key:"pointerUp",value:function(t,e,n,r){var o=this.getPointerIndex(t);-1===o&&(o=this.updatePointer(t,e,n,!1));var i=/cancel$/i.test(e.type)?"cancel":"up";this._scopeFire("interactions:".concat(i),{pointer:t,pointerIndex:o,pointerInfo:this.pointers[o],event:e,eventTarget:n,type:i,curEventTarget:r,interaction:this}),this.simulation||this.end(e),this.removePointer(t,e)}},{key:"documentBlur",value:function(t){this.end(t),this._scopeFire("interactions:blur",{event:t,type:"blur",interaction:this})}},{key:"end",value:function(t){var e;this._ending=!0,t=t||this._latestPointer.event,this.interacting()&&(e=this._doPhase({event:t,interaction:this,phase:"end"})),this._ending=!1,!0===e&&this.stop()}},{key:"currentAction",value:function(){return this._interacting?this.prepared.name:null}},{key:"interacting",value:function(){return this._interacting}},{key:"stop",value:function(){this._scopeFire("interactions:stop",{interaction:this}),this.interactable=this.element=null,this._interacting=!1,this._stopped=!0,this.prepared.name=this.prevEvent=null}},{key:"getPointerIndex",value:function(t){var e=B.getPointerId(t);return"mouse"===this.pointerType||"pen"===this.pointerType?this.pointers.length-1:Z.findIndex(this.pointers,(function(t){return t.id===e}))}},{key:"getPointerInfo",value:function(t){return this.pointers[this.getPointerIndex(t)]}},{key:"updatePointer",value:function(t,e,n,r){var o=B.getPointerId(t),i=this.getPointerIndex(t),a=this.pointers[i];return r=!1!==r&&(r||/(down|start)$/i.test(e.type)),a?a.pointer=t:(a=new Xe.PointerInfo(o,t,e,null,null),i=this.pointers.length,this.pointers.push(a)),B.setCoords(this.coords.cur,this.pointers.map((function(t){return t.pointer})),this._now()),B.setCoordDeltas(this.coords.delta,this.coords.prev,this.coords.cur),r&&(this.pointerIsDown=!0,a.downTime=this.coords.cur.timeStamp,a.downTarget=n,B.pointerExtend(this.downPointer,t),this.interacting()||(B.copyCoords(this.coords.start,this.coords.cur),B.copyCoords(this.coords.prev,this.coords.cur),this.downEvent=e,this.pointerWasMoved=!1)),this._updateLatestPointer(t,e,n),this._scopeFire("interactions:update-pointer",{pointer:t,event:e,eventTarget:n,down:r,pointerInfo:a,pointerIndex:i,interaction:this}),i}},{key:"removePointer",value:function(t,e){var n=this.getPointerIndex(t);if(-1!==n){var r=this.pointers[n];this._scopeFire("interactions:remove-pointer",{pointer:t,event:e,eventTarget:null,pointerIndex:n,pointerInfo:r,interaction:this}),this.pointers.splice(n,1),this.pointerIsDown=!1}}},{key:"_updateLatestPointer",value:function(t,e,n){this._latestPointer.pointer=t,this._latestPointer.event=e,this._latestPointer.eventTarget=n}},{key:"destroy",value:function(){this._latestPointer.pointer=null,this._latestPointer.event=null,this._latestPointer.eventTarget=null}},{key:"_createPreparedEvent",value:function(t,e,n,r){return new je.InteractEvent(this,t,this.prepared.name,e,this.element,n,r)}},{key:"_fireEvent",value:function(t){this.interactable.fire(t),(!this.prevEvent||t.timeStamp>=this.prevEvent.timeStamp)&&(this.prevEvent=t)}},{key:"_doPhase",value:function(t){var e=t.event,n=t.phase,r=t.preEnd,o=t.type,i=this.rect;if(i&&"move"===n&&(k.addEdges(this.edges,i,this.coords.delta[this.interactable.options.deltaSource]),i.width=i.right-i.left,i.height=i.bottom-i.top),!1===this._scopeFire("interactions:before-action-".concat(n),t))return!1;var a=t.iEvent=this._createPreparedEvent(e,n,r,o);return this._scopeFire("interactions:action-".concat(n),t),"start"===n&&(this.prevEvent=a),this._fireEvent(a),this._scopeFire("interactions:after-action-".concat(n),t),!0}},{key:"_now",value:function(){return Date.now()}}])&&Ue(e.prototype,n),t}();Le.Interaction=qe;var $e=qe;Le.default=$e;var Ge={};function He(t){t.pointerIsDown&&(Qe(t.coords.cur,t.offset.total),t.offset.pending.x=0,t.offset.pending.y=0)}function Ke(t){Ze(t.interaction)}function Ze(t){if(!function(t){return!(!t.offset.pending.x&&!t.offset.pending.y)}(t))return!1;var e=t.offset.pending;return Qe(t.coords.cur,e),Qe(t.coords.delta,e),k.addEdges(t.edges,t.rect,e),e.x=0,e.y=0,!0}function Je(t){var e=t.x,n=t.y;this.offset.pending.x+=e,this.offset.pending.y+=n,this.offset.total.x+=e,this.offset.total.y+=n}function Qe(t,e){var n=t.page,r=t.client,o=e.x,i=e.y;n.x+=o,n.y+=i,r.x+=o,r.y+=i}Object.defineProperty(Ge,"__esModule",{value:!0}),Ge.addTotal=He,Ge.applyPending=Ze,Ge.default=void 0,Le._ProxyMethods.offsetBy="";var tn={id:"offset",before:["modifiers","pointer-events","actions","inertia"],install:function(t){t.Interaction.prototype.offsetBy=Je},listeners:{"interactions:new":function(t){t.interaction.offset={total:{x:0,y:0},pending:{x:0,y:0}}},"interactions:update-pointer":function(t){return He(t.interaction)},"interactions:before-action-start":Ke,"interactions:before-action-move":Ke,"interactions:before-action-end":function(t){var e=t.interaction;if(Ze(e))return e.move({offset:!0}),e.end(),!1},"interactions:stop":function(t){var e=t.interaction;e.offset.total.x=0,e.offset.total.y=0,e.offset.pending.x=0,e.offset.pending.y=0}}};Ge.default=tn;var en={};function nn(t,e){for(var n=0;nn.minSpeed&&o>n.endSpeed)this.startInertia();else{if(i.result=i.setAll(this.modifierArg),!i.result.changed)return!1;this.startSmoothEnd()}return e.modification.result.rect=null,e.offsetBy(this.targetOffset),e._doPhase({interaction:e,event:t,phase:"inertiastart"}),e.offsetBy({x:-this.targetOffset.x,y:-this.targetOffset.y}),e.modification.result.rect=null,this.active=!0,e.simulation=this,!0}},{key:"startInertia",value:function(){var t=this,e=this.interaction.coords.velocity.client,n=an(this.interaction),r=n.resistance,o=-Math.log(n.endSpeed/this.v0)/r;this.targetOffset={x:(e.x-o)/r,y:(e.y-o)/r},this.te=o,this.lambda_v0=r/this.v0,this.one_ve_v0=1-n.endSpeed/this.v0;var i=this.modification,a=this.modifierArg;a.pageCoords={x:this.startCoords.x+this.targetOffset.x,y:this.startCoords.y+this.targetOffset.y},i.result=i.setAll(a),i.result.changed&&(this.isModified=!0,this.modifiedOffset={x:this.targetOffset.x+i.result.delta.x,y:this.targetOffset.y+i.result.delta.y}),this.onNextFrame((function(){return t.inertiaTick()}))}},{key:"startSmoothEnd",value:function(){var t=this;this.smoothEnd=!0,this.isModified=!0,this.targetOffset={x:this.modification.result.delta.x,y:this.modification.result.delta.y},this.onNextFrame((function(){return t.smoothEndTick()}))}},{key:"onNextFrame",value:function(t){var e=this;this.timeout=jt.default.request((function(){e.active&&t()}))}},{key:"inertiaTick",value:function(){var t,e,n,r,o,i=this,a=this.interaction,s=an(a).resistance,l=(a._now()-this.t0)/1e3;if(l=0;n--){var r=e[n],o=r.selector,a=r.context,s=r.listeners;o===this.target&&a===this._context&&e.splice(n,1);for(var l=s.length-1;l>=0;l--)this._scopeEvents.removeDelegate(this.target,this._context,t,s[l][0],s[l][1])}else this._scopeEvents.remove(this.target,"all")}}])&&mn(n.prototype,r),t}();yn.Interactable=xn;var wn={};function _n(t,e){for(var n=0;nt.length)&&(e=t.length);for(var n=0,r=Array(e);n=0;a--){var p=f[a];if(p.selector===t&&p.context===e){for(var v=p.listeners,h=v.length-1;h>=0;h--){var g=Mn(v[h],2),y=g[0],m=g[1],b=m.capture,x=m.passive;if(y===o&&b===s.capture&&x===s.passive){v.splice(h,1),v.length||(f.splice(a,1),l(e,n,u),l(e,n,c,!0)),d=!0;break}}if(d)break}}},delegateListener:u,delegateUseCapture:c,delegatedEvents:r,documents:o,targets:n,supportsOptions:!1,supportsPassive:!1};function s(t,e,r,o){var i=In(o),s=Z.find(n,(function(e){return e.eventTarget===t}));s||(s={eventTarget:t,events:{}},n.push(s)),s.events[e]||(s.events[e]=[]),t.addEventListener&&!Z.contains(s.events[e],r)&&(t.addEventListener(e,r,a.supportsOptions?i:i.capture),s.events[e].push(r))}function l(t,e,r,o){var i=In(o),s=Z.findIndex(n,(function(e){return e.eventTarget===t})),u=n[s];if(u&&u.events)if("all"!==e){var c=!1,f=u.events[e];if(f){if("all"===r){for(var d=f.length-1;d>=0;d--)l(t,e,f[d],i);return}for(var p=0;p=2)continue;if(!o.interacting()&&e===o.pointerType)return o}return null}};function zn(t,e){return t.pointers.some((function(t){return t.id===e}))}var Cn=Rn;An.default=Cn;var Fn={};function Xn(t){return(Xn="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function Yn(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(t)){var n=[],r=!0,o=!1,i=void 0;try{for(var a,s=t[Symbol.iterator]();!(r=(a=s.next()).done)&&(n.push(a.value),!e||n.length!==e);r=!0);}catch(t){o=!0,i=t}finally{try{r||null==s.return||s.return()}finally{if(o)throw i}}return n}}(t,e)||function(t,e){if(t){if("string"==typeof t)return Bn(t,e);var n=Object.prototype.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?Bn(t,e):void 0}}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function Bn(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,r=Array(e);n=0;r--){var o=e.interactions.list[r];o.interactable===n&&(o.stop(),e.fire("interactions:destroy",{interaction:o}),o.destroy(),e.interactions.list.length>2&&e.interactions.list.splice(r,1))}}},onDocSignal:Hn,doOnInteractions:$n,methodNames:qn};Fn.default=Kn;var Zn={};function Jn(t){return(Jn="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function Qn(t,e,n){return(Qn="undefined"!=typeof Reflect&&Reflect.get?Reflect.get:function(t,e,n){var r=function(t,e){for(;!Object.prototype.hasOwnProperty.call(t,e)&&null!==(t=nr(t)););return t}(t,e);if(r){var o=Object.getOwnPropertyDescriptor(r,e);return o.get?o.get.call(n):o.value}})(t,e,n||t)}function tr(t,e){return(tr=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}function er(t,e){return!e||"object"!==Jn(e)&&"function"!=typeof e?function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t):e}function nr(t){return(nr=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)})(t)}function rr(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function or(t,e){for(var n=0;nt.length)&&(e=t.length);for(var n=0,r=Array(e);nMath.abs(l.y),s.coords,s.rect),(0,j.default)(r,s.coords)),s.eventProps},defaults:{ratio:"preserve",equalDelta:!1,modifiers:[],enabled:!1}};function Tr(t,e,n){var r=t.startCoords,o=t.edgeSign;e?n.y=r.y+(n.x-r.x)*o:n.x=r.x+(n.y-r.y)*o}function Mr(t,e,n,r){var o=t.startRect,i=t.startCoords,a=t.ratio,s=t.edgeSign;if(e){var l=r.width/a;n.y=i.y+(l-o.height)*s}else{var u=r.height*a;n.x=i.x+(u-o.width)*s}}_r.aspectRatio=Er;var jr=(0,Se.makeModifier)(Er,"aspectRatio");_r.default=jr;var kr={};Object.defineProperty(kr,"__esModule",{value:!0}),kr.default=void 0;var Ir=function(){};Ir._defaults={};var Dr=Ir;kr.default=Dr;var Ar={};Object.defineProperty(Ar,"__esModule",{value:!0}),Object.defineProperty(Ar,"default",{enumerable:!0,get:function(){return kr.default}});var Rr={};function zr(t,e,n){return i.default.func(t)?k.resolveRectLike(t,e.interactable,e.element,[n.x,n.y,e]):k.resolveRectLike(t,e.interactable,e.element)}Object.defineProperty(Rr,"__esModule",{value:!0}),Rr.getRestrictionRect=zr,Rr.restrict=Rr.default=void 0;var Cr={start:function(t){var e=t.rect,n=t.startOffset,r=t.state,o=t.interaction,i=t.pageCoords,a=r.options,s=a.elementRect,l=(0,j.default)({left:0,top:0,right:0,bottom:0},a.offset||{});if(e&&s){var u=zr(a.restriction,o,i);if(u){var c=u.right-u.left-e.width,f=u.bottom-u.top-e.height;c<0&&(l.left+=c,l.right+=c),f<0&&(l.top+=f,l.bottom+=f)}l.left+=n.left-e.width*s.left,l.top+=n.top-e.height*s.top,l.right+=n.right-e.width*(1-s.right),l.bottom+=n.bottom-e.height*(1-s.bottom)}r.offset=l},set:function(t){var e=t.coords,n=t.interaction,r=t.state,o=r.options,i=r.offset,a=zr(o.restriction,n,e);if(a){var s=k.xywhToTlbr(a);e.x=Math.max(Math.min(s.right-i.right,e.x),s.left+i.left),e.y=Math.max(Math.min(s.bottom-i.bottom,e.y),s.top+i.top)}},defaults:{restriction:null,elementRect:null,offset:null,endOnly:!1,enabled:!1}};Rr.restrict=Cr;var Fr=(0,Se.makeModifier)(Cr,"restrict");Rr.default=Fr;var Xr={};Object.defineProperty(Xr,"__esModule",{value:!0}),Xr.restrictEdges=Xr.default=void 0;var Yr={top:1/0,left:1/0,bottom:-1/0,right:-1/0},Br={top:-1/0,left:-1/0,bottom:1/0,right:1/0};function Wr(t,e){for(var n=["top","left","bottom","right"],r=0;rt.length)&&(e=t.length);for(var n=0,r=Array(e);n zipThreshold; + + let jszip = null; + if (zipFiles) { + jszip = new JSZip(); + } + + const promises = Array.from(files).map(async file => { + const { processedData, fileName } = await processFileCallback(file); + + if (zipFiles) { + jszip.file(fileName, processedData); + } else { + const url = URL.createObjectURL(processedData); + const downloadOption = localStorage.getItem('downloadOption'); + + if (downloadOption === 'sameWindow') { + window.location.href = url; + } else if (downloadOption === 'newWindow') { + window.open(url, '_blank'); + } else { + const downloadLink = document.createElement('a'); + downloadLink.href = url; + downloadLink.download = fileName; + downloadLink.click(); + } + } + }); + + await Promise.all(promises); + + if (zipFiles) { + const content = await jszip.generateAsync({ type: "blob" }); + const url = URL.createObjectURL(content); + const a = document.createElement('a'); + a.href = url; + a.download = "files.zip"; + document.body.appendChild(a); + a.click(); + a.remove(); + } +} diff --git a/src/main/resources/static/js/signature_pad.umd.min.js b/src/main/resources/static/js/signature_pad.umd.min.js new file mode 100644 index 00000000..a895fd11 --- /dev/null +++ b/src/main/resources/static/js/signature_pad.umd.min.js @@ -0,0 +1,6 @@ +/*! + * Signature Pad v4.1.5 | https://github.com/szimek/signature_pad + * (c) 2023 Szymon Nowak | Released under the MIT license + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).SignaturePad=e()}(this,(function(){"use strict";class t{constructor(t,e,i,s){if(isNaN(t)||isNaN(e))throw new Error(`Point is invalid: (${t}, ${e})`);this.x=+t,this.y=+e,this.pressure=i||0,this.time=s||Date.now()}distanceTo(t){return Math.sqrt(Math.pow(this.x-t.x,2)+Math.pow(this.y-t.y,2))}equals(t){return this.x===t.x&&this.y===t.y&&this.pressure===t.pressure&&this.time===t.time}velocityFrom(t){return this.time!==t.time?this.distanceTo(t)/(this.time-t.time):0}}class e{constructor(t,e,i,s,n,o){this.startPoint=t,this.control2=e,this.control1=i,this.endPoint=s,this.startWidth=n,this.endWidth=o}static fromPoints(t,i){const s=this.calculateControlPoints(t[0],t[1],t[2]).c2,n=this.calculateControlPoints(t[1],t[2],t[3]).c1;return new e(t[1],s,n,t[2],i.start,i.end)}static calculateControlPoints(e,i,s){const n=e.x-i.x,o=e.y-i.y,h=i.x-s.x,r=i.y-s.y,a=(e.x+i.x)/2,d=(e.y+i.y)/2,c=(i.x+s.x)/2,l=(i.y+s.y)/2,u=Math.sqrt(n*n+o*o),v=Math.sqrt(h*h+r*r),_=v/(u+v),m=c+(a-c)*_,p=l+(d-l)*_,g=i.x-m,w=i.y-p;return{c1:new t(a+g,d+w),c2:new t(c+g,l+w)}}length(){let t,e,i=0;for(let s=0;s<=10;s+=1){const n=s/10,o=this.point(n,this.startPoint.x,this.control1.x,this.control2.x,this.endPoint.x),h=this.point(n,this.startPoint.y,this.control1.y,this.control2.y,this.endPoint.y);if(s>0){const s=o-t,n=h-e;i+=Math.sqrt(s*s+n*n)}t=o,e=h}return i}point(t,e,i,s,n){return e*(1-t)*(1-t)*(1-t)+3*i*(1-t)*(1-t)*t+3*s*(1-t)*t*t+n*t*t*t}}class i{constructor(){try{this._et=new EventTarget}catch(t){this._et=document}}addEventListener(t,e,i){this._et.addEventListener(t,e,i)}dispatchEvent(t){return this._et.dispatchEvent(t)}removeEventListener(t,e,i){this._et.removeEventListener(t,e,i)}}class s extends i{constructor(t,e={}){super(),this.canvas=t,this._drawningStroke=!1,this._isEmpty=!0,this._lastPoints=[],this._data=[],this._lastVelocity=0,this._lastWidth=0,this._handleMouseDown=t=>{1===t.buttons&&(this._drawningStroke=!0,this._strokeBegin(t))},this._handleMouseMove=t=>{this._drawningStroke&&this._strokeMoveUpdate(t)},this._handleMouseUp=t=>{1===t.buttons&&this._drawningStroke&&(this._drawningStroke=!1,this._strokeEnd(t))},this._handleTouchStart=t=>{if(t.cancelable&&t.preventDefault(),1===t.targetTouches.length){const e=t.changedTouches[0];this._strokeBegin(e)}},this._handleTouchMove=t=>{t.cancelable&&t.preventDefault();const e=t.targetTouches[0];this._strokeMoveUpdate(e)},this._handleTouchEnd=t=>{if(t.target===this.canvas){t.cancelable&&t.preventDefault();const e=t.changedTouches[0];this._strokeEnd(e)}},this._handlePointerStart=t=>{this._drawningStroke=!0,t.preventDefault(),this._strokeBegin(t)},this._handlePointerMove=t=>{this._drawningStroke&&(t.preventDefault(),this._strokeMoveUpdate(t))},this._handlePointerEnd=t=>{this._drawningStroke&&(t.preventDefault(),this._drawningStroke=!1,this._strokeEnd(t))},this.velocityFilterWeight=e.velocityFilterWeight||.7,this.minWidth=e.minWidth||.5,this.maxWidth=e.maxWidth||2.5,this.throttle="throttle"in e?e.throttle:16,this.minDistance="minDistance"in e?e.minDistance:5,this.dotSize=e.dotSize||0,this.penColor=e.penColor||"black",this.backgroundColor=e.backgroundColor||"rgba(0,0,0,0)",this._strokeMoveUpdate=this.throttle?function(t,e=250){let i,s,n,o=0,h=null;const r=()=>{o=Date.now(),h=null,i=t.apply(s,n),h||(s=null,n=[])};return function(...a){const d=Date.now(),c=e-(d-o);return s=this,n=a,c<=0||c>e?(h&&(clearTimeout(h),h=null),o=d,i=t.apply(s,n),h||(s=null,n=[])):h||(h=window.setTimeout(r,c)),i}}(s.prototype._strokeUpdate,this.throttle):s.prototype._strokeUpdate,this._ctx=t.getContext("2d"),this.clear(),this.on()}clear(){const{_ctx:t,canvas:e}=this;t.fillStyle=this.backgroundColor,t.clearRect(0,0,e.width,e.height),t.fillRect(0,0,e.width,e.height),this._data=[],this._reset(this._getPointGroupOptions()),this._isEmpty=!0}fromDataURL(t,e={}){return new Promise(((i,s)=>{const n=new Image,o=e.ratio||window.devicePixelRatio||1,h=e.width||this.canvas.width/o,r=e.height||this.canvas.height/o,a=e.xOffset||0,d=e.yOffset||0;this._reset(this._getPointGroupOptions()),n.onload=()=>{this._ctx.drawImage(n,a,d,h,r),i()},n.onerror=t=>{s(t)},n.crossOrigin="anonymous",n.src=t,this._isEmpty=!1}))}toDataURL(t="image/png",e){return"image/svg+xml"===t?("object"!=typeof e&&(e=void 0),`data:image/svg+xml;base64,${btoa(this.toSVG(e))}`):("number"!=typeof e&&(e=void 0),this.canvas.toDataURL(t,e))}on(){this.canvas.style.touchAction="none",this.canvas.style.msTouchAction="none",this.canvas.style.userSelect="none";const t=/Macintosh/.test(navigator.userAgent)&&"ontouchstart"in document;window.PointerEvent&&!t?this._handlePointerEvents():(this._handleMouseEvents(),"ontouchstart"in window&&this._handleTouchEvents())}off(){this.canvas.style.touchAction="auto",this.canvas.style.msTouchAction="auto",this.canvas.style.userSelect="auto",this.canvas.removeEventListener("pointerdown",this._handlePointerStart),this.canvas.removeEventListener("pointermove",this._handlePointerMove),this.canvas.ownerDocument.removeEventListener("pointerup",this._handlePointerEnd),this.canvas.removeEventListener("mousedown",this._handleMouseDown),this.canvas.removeEventListener("mousemove",this._handleMouseMove),this.canvas.ownerDocument.removeEventListener("mouseup",this._handleMouseUp),this.canvas.removeEventListener("touchstart",this._handleTouchStart),this.canvas.removeEventListener("touchmove",this._handleTouchMove),this.canvas.removeEventListener("touchend",this._handleTouchEnd)}isEmpty(){return this._isEmpty}fromData(t,{clear:e=!0}={}){e&&this.clear(),this._fromData(t,this._drawCurve.bind(this),this._drawDot.bind(this)),this._data=this._data.concat(t)}toData(){return this._data}_getPointGroupOptions(t){return{penColor:t&&"penColor"in t?t.penColor:this.penColor,dotSize:t&&"dotSize"in t?t.dotSize:this.dotSize,minWidth:t&&"minWidth"in t?t.minWidth:this.minWidth,maxWidth:t&&"maxWidth"in t?t.maxWidth:this.maxWidth,velocityFilterWeight:t&&"velocityFilterWeight"in t?t.velocityFilterWeight:this.velocityFilterWeight}}_strokeBegin(t){this.dispatchEvent(new CustomEvent("beginStroke",{detail:t}));const e=this._getPointGroupOptions(),i=Object.assign(Object.assign({},e),{points:[]});this._data.push(i),this._reset(e),this._strokeUpdate(t)}_strokeUpdate(t){if(0===this._data.length)return void this._strokeBegin(t);this.dispatchEvent(new CustomEvent("beforeUpdateStroke",{detail:t}));const e=t.clientX,i=t.clientY,s=void 0!==t.pressure?t.pressure:void 0!==t.force?t.force:0,n=this._createPoint(e,i,s),o=this._data[this._data.length-1],h=o.points,r=h.length>0&&h[h.length-1],a=!!r&&n.distanceTo(r)<=this.minDistance,d=this._getPointGroupOptions(o);if(!r||!r||!a){const t=this._addPoint(n,d);r?t&&this._drawCurve(t,d):this._drawDot(n,d),h.push({time:n.time,x:n.x,y:n.y,pressure:n.pressure})}this.dispatchEvent(new CustomEvent("afterUpdateStroke",{detail:t}))}_strokeEnd(t){this._strokeUpdate(t),this.dispatchEvent(new CustomEvent("endStroke",{detail:t}))}_handlePointerEvents(){this._drawningStroke=!1,this.canvas.addEventListener("pointerdown",this._handlePointerStart),this.canvas.addEventListener("pointermove",this._handlePointerMove),this.canvas.ownerDocument.addEventListener("pointerup",this._handlePointerEnd)}_handleMouseEvents(){this._drawningStroke=!1,this.canvas.addEventListener("mousedown",this._handleMouseDown),this.canvas.addEventListener("mousemove",this._handleMouseMove),this.canvas.ownerDocument.addEventListener("mouseup",this._handleMouseUp)}_handleTouchEvents(){this.canvas.addEventListener("touchstart",this._handleTouchStart),this.canvas.addEventListener("touchmove",this._handleTouchMove),this.canvas.addEventListener("touchend",this._handleTouchEnd)}_reset(t){this._lastPoints=[],this._lastVelocity=0,this._lastWidth=(t.minWidth+t.maxWidth)/2,this._ctx.fillStyle=t.penColor}_createPoint(e,i,s){const n=this.canvas.getBoundingClientRect();return new t(e-n.left,i-n.top,s,(new Date).getTime())}_addPoint(t,i){const{_lastPoints:s}=this;if(s.push(t),s.length>2){3===s.length&&s.unshift(s[0]);const t=this._calculateCurveWidths(s[1],s[2],i),n=e.fromPoints(s,t);return s.shift(),n}return null}_calculateCurveWidths(t,e,i){const s=i.velocityFilterWeight*e.velocityFrom(t)+(1-i.velocityFilterWeight)*this._lastVelocity,n=this._strokeWidth(s,i),o={end:n,start:this._lastWidth};return this._lastVelocity=s,this._lastWidth=n,o}_strokeWidth(t,e){return Math.max(e.maxWidth/(t+1),e.minWidth)}_drawCurveSegment(t,e,i){const s=this._ctx;s.moveTo(t,e),s.arc(t,e,i,0,2*Math.PI,!1),this._isEmpty=!1}_drawCurve(t,e){const i=this._ctx,s=t.endWidth-t.startWidth,n=2*Math.ceil(t.length());i.beginPath(),i.fillStyle=e.penColor;for(let i=0;i0?e.dotSize:(e.minWidth+e.maxWidth)/2;i.beginPath(),this._drawCurveSegment(t.x,t.y,s),i.closePath(),i.fillStyle=e.penColor,i.fill()}_fromData(e,i,s){for(const n of e){const{points:e}=n,o=this._getPointGroupOptions(n);if(e.length>1)for(let s=0;s{const i=document.createElement("path");if(!(isNaN(t.control1.x)||isNaN(t.control1.y)||isNaN(t.control2.x)||isNaN(t.control2.y))){const s=`M ${t.startPoint.x.toFixed(3)},${t.startPoint.y.toFixed(3)} C ${t.control1.x.toFixed(3)},${t.control1.y.toFixed(3)} ${t.control2.x.toFixed(3)},${t.control2.y.toFixed(3)} ${t.endPoint.x.toFixed(3)},${t.endPoint.y.toFixed(3)}`;i.setAttribute("d",s),i.setAttribute("stroke-width",(2.25*t.endWidth).toFixed(3)),i.setAttribute("stroke",e),i.setAttribute("fill","none"),i.setAttribute("stroke-linecap","round"),o.appendChild(i)}}),((t,{penColor:e,dotSize:i,minWidth:s,maxWidth:n})=>{const h=document.createElement("circle"),r=i>0?i:(s+n)/2;h.setAttribute("r",r.toString()),h.setAttribute("cx",t.x.toString()),h.setAttribute("cy",t.y.toString()),h.setAttribute("fill",e),o.appendChild(h)})),o.outerHTML}}return s})); +//# sourceMappingURL=signature_pad.umd.min.js.map diff --git a/src/main/resources/static/js/tab-container.js b/src/main/resources/static/js/tab-container.js new file mode 100644 index 00000000..6ae7162c --- /dev/null +++ b/src/main/resources/static/js/tab-container.js @@ -0,0 +1,39 @@ + +TabContainer = { + initTabGroups() { + const groups = document.querySelectorAll(".tab-group"); + const unloadedGroups = [...groups].filter(g => !g.initialised); + unloadedGroups.forEach(group => { + const containers = group.querySelectorAll(".tab-container"); + const tabTitles = [...containers].map(c => c.getAttribute("title")); + + const tabList = document.createElement("div"); + tabList.classList.add("tab-buttons"); + tabTitles.forEach(title => { + const tabButton = document.createElement("button"); + tabButton.innerHTML = title; + tabButton.onclick = e => { + this.setActiveTab(e.target); + } + tabList.appendChild(tabButton); + }); + group.prepend(tabList); + + this.setActiveTab(tabList.firstChild); + + group.initialised = true; + }); + }, + setActiveTab(tabButton) { + const group = tabButton.closest(".tab-group") + + group.querySelectorAll(".active").forEach(el => el.classList.remove("active")); + + tabButton.classList.add("active"); + group.querySelector(`[title="${tabButton.innerHTML}"]`).classList.add("active"); + }, +} + +document.addEventListener("DOMContentLoaded", () => { + TabContainer.initTabGroups(); +}) \ No newline at end of file diff --git a/src/main/resources/templates/convert/file-to-pdf.html b/src/main/resources/templates/convert/file-to-pdf.html index 2aab4bf5..f90572f0 100644 --- a/src/main/resources/templates/convert/file-to-pdf.html +++ b/src/main/resources/templates/convert/file-to-pdf.html @@ -5,6 +5,7 @@ +
diff --git a/src/main/resources/templates/convert/img-to-pdf.html b/src/main/resources/templates/convert/img-to-pdf.html index afd704dc..b64f43f1 100644 --- a/src/main/resources/templates/convert/img-to-pdf.html +++ b/src/main/resources/templates/convert/img-to-pdf.html @@ -5,6 +5,7 @@ +
@@ -28,6 +29,14 @@
+
+ + +

@@ -40,7 +49,7 @@

- + + - - - +
+
Lives: 3
+
Score: 0
+
High Score: 0
+
Level: 1
+ +
+ + +
+ + + - - - - - - - -
-
- - -
-
-
-
+
+ + +
+
+
+ - - - - - - - - - \ 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 51d19e22..5dc7f369 100644 --- a/src/main/resources/templates/fragments/navbar.html +++ b/src/main/resources/templates/fragments/navbar.html @@ -259,7 +259,7 @@ function compareVersions(version1, version2) { -
@@ -340,6 +364,48 @@ function compareVersions(version1, version2) {
+ +
@@ -389,6 +455,8 @@ function compareVersions(version1, version2) {
diff --git a/src/main/resources/templates/multi-tool.html b/src/main/resources/templates/multi-tool.html index fc309162..50fadcca 100644 --- a/src/main/resources/templates/multi-tool.html +++ b/src/main/resources/templates/multi-tool.html @@ -99,7 +99,7 @@ gap: 10px; align-items: start; - background: rgba(13, 110, 253, 0.1); + background-color: rgba(13, 110, 253, 0.1); border: 1px solid rgba(0, 0, 0, .25); backdrop-filter: blur(2px); diff --git a/src/main/resources/templates/other/add-image.html b/src/main/resources/templates/other/add-image.html index 499cad6e..5e800481 100644 --- a/src/main/resources/templates/other/add-image.html +++ b/src/main/resources/templates/other/add-image.html @@ -1,10 +1,11 @@ - - - - + + + + +
@@ -14,23 +15,121 @@

-
-
-
- - + + +
+ + +
+
+
+ +
+
+ + +
+ + +
+ + +
-
- -
-
- -
-
- -
- - + +
+ + +
+ +
+
@@ -38,5 +137,4 @@
- \ No newline at end of file diff --git a/src/main/resources/templates/other/compare.html b/src/main/resources/templates/other/compare.html new file mode 100644 index 00000000..042af9b7 --- /dev/null +++ b/src/main/resources/templates/other/compare.html @@ -0,0 +1,190 @@ + + + + + + + + +
+
+
+

+
+
+
+

+ +
+
+ + + +
+
+

+
+
+
+

+
+
+
+ + +
+
+
+
+
+
\ No newline at end of file diff --git a/src/main/resources/templates/other/compress-pdf.html b/src/main/resources/templates/other/compress-pdf.html index 189dd993..f173dba4 100644 --- a/src/main/resources/templates/other/compress-pdf.html +++ b/src/main/resources/templates/other/compress-pdf.html @@ -6,6 +6,7 @@ +
diff --git a/src/main/resources/templates/other/flatten.html b/src/main/resources/templates/other/flatten.html new file mode 100644 index 00000000..5fab1b5f --- /dev/null +++ b/src/main/resources/templates/other/flatten.html @@ -0,0 +1,57 @@ + + + + + + + + +
+
+
+

+
+
+
+

+
+
+
+
+ + + + +
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/other/ocr-pdf.html b/src/main/resources/templates/other/ocr-pdf.html index 3e36b5eb..d9c400bb 100644 --- a/src/main/resources/templates/other/ocr-pdf.html +++ b/src/main/resources/templates/other/ocr-pdf.html @@ -6,6 +6,7 @@ +
diff --git a/src/main/resources/templates/other/remove-blanks.html b/src/main/resources/templates/other/remove-blanks.html new file mode 100644 index 00000000..1bfffc0b --- /dev/null +++ b/src/main/resources/templates/other/remove-blanks.html @@ -0,0 +1,39 @@ + + + + + + + +
+
+
+

+
+
+
+

+ +
+
+
+ + + +
+
+ + + +
+ +
+
+
+
+
+
+
+ + + diff --git a/src/main/resources/templates/other/repair.html b/src/main/resources/templates/other/repair.html new file mode 100644 index 00000000..8ce329b6 --- /dev/null +++ b/src/main/resources/templates/other/repair.html @@ -0,0 +1,29 @@ + + + + + + + + +
+
+
+

+
+
+
+

+
+
+ +
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/sign.html b/src/main/resources/templates/sign.html new file mode 100644 index 00000000..33ba5914 --- /dev/null +++ b/src/main/resources/templates/sign.html @@ -0,0 +1,291 @@ + + + + + + + + + + + +
+
+
+

+
+
+
+

+ + +
+ + +
+
+
+ +
+
+ +
+ + + + +
+
+ + + + +
+ +
+ + +
+
+ + +
+ + +
+ + + +
+ +
+ + +
+ +
+ +
+
+
+
+
+
+ + \ No newline at end of file