4
.gitignore
vendored
|
@ -109,4 +109,6 @@ local.properties
|
|||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
/build
|
||||
/build
|
||||
|
||||
/.vscode
|
|
@ -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
|
||||
```
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="pl_PL">Polish</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="pl_PL">
|
||||
<img src="images/flags/pl.svg" alt="icon" width="20" height="15"> Polski
|
||||
</a>
|
||||
```
|
||||
The data-language-code is the code used to reference the file in the next step.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
137
LocalRunGuide.md
Normal file
|
@ -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"
|
||||
```
|
||||
|
67
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
|
||||
|
|
|
@ -5,7 +5,7 @@ plugins {
|
|||
}
|
||||
|
||||
group = 'stirling.software'
|
||||
version = '0.7.0'
|
||||
version = '0.8.0'
|
||||
sourceCompatibility = '17'
|
||||
|
||||
repositories {
|
||||
|
|
40
scripts/detect-blank-pages.py
Normal file
|
@ -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)
|
|
@ -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."));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<byte[]> 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<byte[]> 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<PDDocument> documents = new ArrayList<>();
|
||||
|
||||
// Loop through the files array and read each file into a PDDocument
|
||||
|
|
|
@ -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<byte[]> 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<byte[]> deletePages(
|
||||
@RequestPart(required = true, value = "fileInput")
|
||||
@Parameter(description = "The input PDF file from which pages will be removed")
|
||||
MultipartFile pdfFile,
|
||||
@RequestParam("pagesToDelete")
|
||||
@Parameter(description = "Comma-separated list of pages or page ranges to delete, e.g., '1,3,5-8'")
|
||||
String pagesToDelete) 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<byte[]> 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<byte[]> 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());
|
||||
|
|
|
@ -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<byte[]> 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<byte[]> 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());
|
||||
|
||||
|
|
|
@ -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<Resource> 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<Resource> 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
|
||||
|
|
|
@ -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<Resource> 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<Resource> 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<byte[]> 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<byte[]> 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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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<byte[]> 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<byte[]> 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();
|
||||
|
||||
|
|
|
@ -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<byte[]> processPdfToHTML(
|
||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to HTML format", required = true) MultipartFile inputFile)
|
||||
throws IOException, InterruptedException {
|
||||
PDFToFile pdfToFile = new PDFToFile();
|
||||
return pdfToFile.processPdfToOfficeFormat(inputFile, "html", "writer_pdf_import");
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-html")
|
||||
public ResponseEntity<byte[]> 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<byte[]> processPdfToPresentation(
|
||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile,
|
||||
@RequestParam("outputFormat") @Parameter(description = "The output Presentation format", schema = @Schema(allowableValues = {
|
||||
"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<byte[]> 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<byte[]> processPdfToRTForTXT(
|
||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile,
|
||||
@RequestParam("outputFormat") @Parameter(description = "The output Text or RTF format", schema = @Schema(allowableValues = {
|
||||
"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<byte[]> 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<byte[]> processPdfToWord(
|
||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file") MultipartFile inputFile,
|
||||
@RequestParam("outputFormat") @Parameter(description = "The output Word document format", schema = @Schema(allowableValues = {
|
||||
"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<byte[]> 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<byte[]> processPdfToXML(
|
||||
@RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be converted to an XML file", required = true) MultipartFile inputFile)
|
||||
throws IOException, InterruptedException {
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf-to-xml")
|
||||
public ResponseEntity<byte[]> 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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<byte[]> 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<byte[]> 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");
|
||||
|
|
|
@ -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<byte[]> 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<Integer> 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<String> 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<Integer> 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();
|
||||
}
|
||||
}
|
|
@ -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<byte[]> 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<byte[]> 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");
|
||||
|
|
|
@ -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<byte[]> 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<byte[]> 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<String> 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<String> 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);
|
||||
|
|
|
@ -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<byte[]> 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<byte[]> 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());
|
||||
|
|
|
@ -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<byte[]> 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<String, String> 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<byte[]> 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<String, String> allRequestParams)
|
||||
throws IOException {
|
||||
|
||||
// Load the PDF file into a PDDocument
|
||||
PDDocument document = PDDocument.load(pdfFile.getBytes());
|
||||
|
|
|
@ -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<byte[]> processPdfWithOCR(@RequestPart(required = true, value = "fileInput") MultipartFile inputFile,
|
||||
@RequestParam("languages") List<String> 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<byte[]> 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<String> 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()) {
|
||||
|
|
|
@ -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<byte[]> 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<byte[]> 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();
|
||||
|
|
|
@ -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<byte[]> 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<String> 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<byte[]> 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<byte[]> 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<byte[]> 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<byte[]> 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();
|
||||
|
|
|
@ -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<byte[]> 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<byte[]> 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<byte[]> 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<PDAnnotation> annotations = page.getAnnotations();
|
||||
List<PDAnnotation> 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<PDField> 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");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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: /";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<String> 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";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
130
src/main/java/stirling/software/SPDF/utils/ImageFinder.java
Normal file
|
@ -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<COSBase> 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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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<COSBase> 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);
|
||||
}
|
||||
}
|
|
@ -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=إضافة صورة
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)。
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
5
src/main/resources/static/css/light-mode.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
/* Dark Mode Styles */
|
||||
body {
|
||||
--body-background-color: 255, 255, 255;
|
||||
--base-font-color: 33, 37, 41;
|
||||
}
|
|
@ -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 {
|
||||
|
|
26
src/main/resources/static/css/tab-container.css
Normal file
|
@ -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));
|
||||
}
|
BIN
src/main/resources/static/fonts/Estonia.woff2
Normal file
BIN
src/main/resources/static/fonts/Tangerine.woff2
Normal file
3
src/main/resources/static/images/blank-file.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file" viewBox="0 0 16 16">
|
||||
<path d="M4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H4zm0 1h8a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 284 B |
4
src/main/resources/static/images/flags/es-ct.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-es-ct" viewBox="0 0 640 480">
|
||||
<path fill="#fcdd09" d="M0 0h640v480H0z"/>
|
||||
<path stroke="#da121a" stroke-width="60" d="M0 90h810m0 120H0m0 120h810m0 120H0" transform="scale(.79012 .88889)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 255 B |
41
src/main/resources/static/images/flatten.svg
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
class="bi bi-ui-checks"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
id="svg826"
|
||||
sodipodi:docname="no-checklist-black.svg"
|
||||
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs830" />
|
||||
<sodipodi:namedview
|
||||
id="namedview828"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#999999"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="55.8125"
|
||||
inkscape:cx="12.452408"
|
||||
inkscape:cy="11.950728"
|
||||
inkscape:current-layer="svg826" />
|
||||
<path
|
||||
d="M 7,2.5 C 7,2.2238576 7.2238576,2 7.5,2 h 7 C 14.776142,2 15,2.2238576 15,2.5 v 1 C 15,3.7761424 14.776142,4 14.5,4 h -7 C 7.2238576,4 7,3.7761424 7,3.5 Z M 2,1 C 0.8954305,1 0,1.8954305 0,3 V 5 C 0,6.1045695 0.8954305,7 2,7 H 4 C 5.1045695,7 6,6.1045695 6,5 V 3 C 6,1.8954305 5.1045695,1 4,1 Z M 2,9 C 0.8954305,9 0,9.8954305 0,11 v 2 c 0,1.104569 0.8954305,2 2,2 h 2 c 1.1045695,0 2,-0.895431 2,-2 V 11 C 6,9.8954305 5.1045695,9 4,9 Z M 2.854,5.354 c -0.1953639,0.1958584 -0.5126361,0.1958584 -0.708,0 l -1,-1 C 0.6740002,3.8820002 1.3820002,3.1740002 1.854,3.646 L 2.5,4.293 4.146,2.646 C 4.6179998,2.1740002 5.3259998,2.8820002 4.854,3.354 Z m 0,8 c -0.1953639,0.195858 -0.5126361,0.195858 -0.708,0 l -1,-1 C 0.67400022,11.882 1.3820002,11.174 1.854,11.646 L 2.5,12.293 4.146,10.646 c 0.4719998,-0.472 1.1799998,0.236 0.708,0.708 z M 7,10.5 C 7,10.223858 7.2238576,10 7.5,10 h 7 c 0.276142,0 0.5,0.223858 0.5,0.5 v 1 c 0,0.276142 -0.223858,0.5 -0.5,0.5 h -7 C 7.2238576,12 7,11.776142 7,11.5 Z m 0,-5 C 7,5.2238576 7.2238576,5 7.5,5 h 5 c 0.666666,0 0.666666,1 0,1 h -5 C 7.2238576,6 7,5.7761424 7,5.5 Z m 0,8 C 7,13.223858 7.2238576,13 7.5,13 h 5 c 0.666666,0 0.666666,1 0,1 h -5 C 7.2238576,14 7,13.776142 7,13.5 Z"
|
||||
id="path824"
|
||||
sodipodi:nodetypes="sssssssssssssssssssssssssssccscccscccscccscsssssssssssssssssssss"
|
||||
style="fill:#000000" />
|
||||
<path
|
||||
d="m 0.14896862,0.16984545 c 0.1879493,-0.2023109 0.51141627,-0.22131574 0.7066285,-0.026004 L 15.795251,15.091111 c 0.471284,0.471524 -0.209339,1.204159 -0.680625,0.732632 L 0.17497212,0.87647378 C -0.02024026,0.68116189 -0.03898081,0.37215613 0.14896862,0.16984545 Z"
|
||||
id="path937"
|
||||
sodipodi:nodetypes="ssssss"
|
||||
style="fill:#000000" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
94
src/main/resources/static/images/scales.svg
Normal file
|
@ -0,0 +1,94 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
|
||||
<svg
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:cc="http://web.resource.org/cc/"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:ns1="http://sozi.baierouge.fr"
|
||||
id="Layer_1"
|
||||
style="enable-background:new 0 0 333.33 287.889"
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 333.33 287.889"
|
||||
version="1.1"
|
||||
y="0px"
|
||||
x="0px"
|
||||
>
|
||||
<g
|
||||
>
|
||||
<polygon
|
||||
style="fill:none"
|
||||
points="19.374 136.98 98.932 136.98 59.083 18.273"
|
||||
/>
|
||||
<polygon
|
||||
style="fill:none"
|
||||
points="235.57 191.34 315.13 191.34 275.28 72.635"
|
||||
/>
|
||||
<path
|
||||
d="m318.63 191.34l-42.85-127.63 6.892 1.386c-0.563 2.401 0.902 4.816 3.304 5.41 2.419 0.599 4.869-0.877 5.47-3.298 0.602-2.42-0.878-4.868-3.301-5.469-2.095-0.518-4.205 0.523-5.125 2.384l-58.16-17.14-54.361-16.029-0.509-9.298c5.801-0.21 10.444-4.966 10.444-10.818 0.01-5.985-4.85-10.838-10.84-10.838-5.985 0-10.837 4.853-10.837 10.838 0 5.288 3.79 9.686 8.8 10.64l-0.477 8.708-53.87-10.838-60.542-12.179c0.08-2.094-1.315-4.028-3.429-4.552-2.419-0.599-4.867 0.879-5.466 3.299-0.599 2.421 0.877 4.869 3.297 5.468 2.418 0.598 4.865-0.876 5.468-3.292l5.889 1.736-42.537 127.15h-15.88s9.255 20.324 58.069 20.324 54.804-20.324 54.804-20.324h-10.446l-42.535-126.72 51.784 15.269 54.78 16.151-11.76 214.82s0.461 9.693-14.772 12.002c-15.234 2.308-34.621 3.229-43.853 8.309-9.233 5.078-10.617 11.078-10.617 11.078h166.64s-1.386-6-10.615-11.078c-9.234-5.079-28.621-6.001-43.854-8.309-15.233-2.309-14.772-12.002-14.772-12.002l-11.72-213.83 52.188 10.499 51.506 10.362-42.755 127.82h-11.623s9.255 20.324 58.068 20.324 54.804-20.324 54.804-20.324h-14.7zm-219.7-54.36h-79.558l39.709-118.71 39.849 118.71zm136.64 54.36l39.708-118.71 39.85 118.71h-79.56z"
|
||||
/>
|
||||
</g
|
||||
>
|
||||
<metadata
|
||||
><rdf:RDF
|
||||
><cc:Work
|
||||
><dc:format
|
||||
>image/svg+xml</dc:format
|
||||
><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage"
|
||||
/><cc:license
|
||||
rdf:resource="http://creativecommons.org/licenses/publicdomain/"
|
||||
/><dc:publisher
|
||||
><cc:Agent
|
||||
rdf:about="http://openclipart.org/"
|
||||
><dc:title
|
||||
>Openclipart</dc:title
|
||||
></cc:Agent
|
||||
></dc:publisher
|
||||
><dc:title
|
||||
>scales of justice</dc:title
|
||||
><dc:date
|
||||
>2009-06-26T04:35:18</dc:date
|
||||
><dc:description
|
||||
/><dc:source
|
||||
>https://openclipart.org/detail/26849/scales-of-justice-by-johnny_automatic</dc:source
|
||||
><dc:creator
|
||||
><cc:Agent
|
||||
><dc:title
|
||||
>johnny_automatic</dc:title
|
||||
></cc:Agent
|
||||
></dc:creator
|
||||
><dc:subject
|
||||
><rdf:Bag
|
||||
><rdf:li
|
||||
>justice</rdf:li
|
||||
><rdf:li
|
||||
>law</rdf:li
|
||||
><rdf:li
|
||||
>measurement</rdf:li
|
||||
><rdf:li
|
||||
>scales</rdf:li
|
||||
><rdf:li
|
||||
>silhouette</rdf:li
|
||||
><rdf:li
|
||||
>weight</rdf:li
|
||||
></rdf:Bag
|
||||
></dc:subject
|
||||
></cc:Work
|
||||
><cc:License
|
||||
rdf:about="http://creativecommons.org/licenses/publicdomain/"
|
||||
><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Reproduction"
|
||||
/><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#Distribution"
|
||||
/><cc:permits
|
||||
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
|
||||
/></cc:License
|
||||
></rdf:RDF
|
||||
></metadata
|
||||
></svg
|
||||
>
|
After Width: | Height: | Size: 3.8 KiB |
4
src/main/resources/static/images/sign.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-vector-pen" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M10.646.646a.5.5 0 0 1 .708 0l4 4a.5.5 0 0 1 0 .708l-1.902 1.902-.829 3.313a1.5 1.5 0 0 1-1.024 1.073L1.254 14.746 4.358 4.4A1.5 1.5 0 0 1 5.43 3.377l3.313-.828L10.646.646zm-1.8 2.908-3.173.793a.5.5 0 0 0-.358.342l-2.57 8.565 8.567-2.57a.5.5 0 0 0 .34-.357l.794-3.174-3.6-3.6z"/>
|
||||
<path fill-rule="evenodd" d="M2.832 13.228 8 9a1 1 0 1 0-1-1l-4.228 5.168-.026.086.086-.026z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 544 B |
3
src/main/resources/static/images/star-fill.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-star-fill" viewBox="0 0 16 16">
|
||||
<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 399 B |
3
src/main/resources/static/images/star.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-star" viewBox="0 0 16 16">
|
||||
<path d="M2.866 14.85c-.078.444.36.791.746.593l4.39-2.256 4.389 2.256c.386.198.824-.149.746-.592l-.83-4.73 3.522-3.356c.33-.314.16-.888-.282-.95l-4.898-.696L8.465.792a.513.513 0 0 0-.927 0L5.354 5.12l-4.898.696c-.441.062-.612.636-.283.95l3.523 3.356-.83 4.73zm4.905-2.767-3.686 1.894.694-3.957a.565.565 0 0 0-.163-.505L1.71 6.745l4.052-.576a.525.525 0 0 0 .393-.288L8 2.223l1.847 3.658a.525.525 0 0 0 .393.288l4.052.575-2.906 2.77a.565.565 0 0 0-.163.506l.694 3.957-3.686-1.894a.503.503 0 0 0-.461 0z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 635 B |
3
src/main/resources/static/images/wrench.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-wrench" viewBox="0 0 16 16">
|
||||
<path d="M.102 2.223A3.004 3.004 0 0 0 3.78 5.897l6.341 6.252A3.003 3.003 0 0 0 13 16a3 3 0 1 0-.851-5.878L5.897 3.781A3.004 3.004 0 0 0 2.223.1l2.141 2.142L4 4l-1.757.364L.102 2.223zm13.37 9.019.528.026.287.445.445.287.026.529L15 13l-.242.471-.026.529-.445.287-.287.445-.529.026L13 15l-.471-.242-.529-.026-.287-.445-.445-.287-.026-.529L11 13l.242-.471.026-.529.445-.287.287-.445.529-.026L13 11l.471.242z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 541 B |
265
src/main/resources/static/js/draggable-utils.js
Normal file
|
@ -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();
|
||||
});
|
|
@ -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;
|
||||
|
|
3
src/main/resources/static/js/interact.min.js
vendored
Normal file
47
src/main/resources/static/js/local-pdf-input-download.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
async function downloadFilesWithCallback(processFileCallback) {
|
||||
const fileInput = document.querySelector('input[type="file"]');
|
||||
const files = fileInput.files;
|
||||
|
||||
const zipThreshold = 4;
|
||||
const zipFiles = files.length > 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();
|
||||
}
|
||||
}
|
6
src/main/resources/static/js/signature_pad.umd.min.js
vendored
Normal file
39
src/main/resources/static/js/tab-container.js
Normal file
|
@ -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();
|
||||
})
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
|
@ -28,6 +29,14 @@
|
|||
<input type="checkbox" class="form-check-input" name="autoRotate" id="autoRotate">
|
||||
<label class="ml-3" for="autoRotate" th:text=#{imageToPDF.selectText.2}></label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label th:text="#{pdfToImage.colorType}"></label>
|
||||
<select class="form-control" name="colorType">
|
||||
<option value="color" th:text="#{pdfToImage.color}"></option>
|
||||
<option value="greyscale" th:text="#{pdfToImage.grey}"></option>
|
||||
<option value="blackwhite" th:text="#{pdfToImage.blackwhite}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<br>
|
||||
<input type="hidden" id="override" name="override" value="multi">
|
||||
<div class="form-group">
|
||||
|
@ -40,7 +49,7 @@
|
|||
|
||||
<br> <br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{imageToPDF.submit}"></button>
|
||||
<script>
|
||||
<script>
|
||||
$('#fileInput-input').on('change', function() {
|
||||
var files = document.getElementById("fileInput-input").files;
|
||||
var conversionType = document.getElementById("conversionType");
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{PDFToHTML.title})}"></th:block>
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{PDFToPresentation.title})}"></th:block>
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{PDFToText.title})}"></th:block>
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{PDFToWord.title})}"></th:block>
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{PDFToXML.title})}"></th:block>
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div th:fragment="card" class="feature-card">
|
||||
<div th:fragment="card" class="feature-card" th:id="${id}">
|
||||
<a th:href="${cardLink}">
|
||||
<div class="d-flex align-items-center"> <!-- Add a flex container to align the SVG and title -->
|
||||
<img th:if="${svgPath}" id="card-icon" class="home-card-icon home-card-icon-colour" th:src="${svgPath}" alt="Icon" width="30" height="30">
|
||||
|
@ -6,4 +6,7 @@
|
|||
</div>
|
||||
<p class="card-text" th:text="${cardText}"></p>
|
||||
</a>
|
||||
<div class="favorite-icon" onclick="toggleFavorite(this)">
|
||||
<img src="images/star.svg" alt="Favorite">
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -27,8 +27,13 @@
|
|||
|
||||
<!-- Custom -->
|
||||
<link rel="stylesheet" href="css/general.css">
|
||||
<link rel="stylesheet" th:href="@{css/light-mode.css}" id="light-mode-styles">
|
||||
<link rel="stylesheet" th:href="@{css/dark-mode.css}" id="dark-mode-styles">
|
||||
<link rel="stylesheet" th:href="@{css/rainbow-mode.css}" id="rainbow-mode-styles" disabled="true">
|
||||
<link rel="stylesheet" href="css/tab-container.css">
|
||||
<script src="js/tab-container.js"></script>
|
||||
|
||||
|
||||
<script>
|
||||
var toggleCount = 0;
|
||||
var lastToggleTime = Date.now();
|
||||
|
@ -42,22 +47,26 @@ function toggleDarkMode() {
|
|||
}
|
||||
lastToggleTime = currentTime;
|
||||
|
||||
var lightModeStyles = document.getElementById("light-mode-styles");
|
||||
var darkModeStyles = document.getElementById("dark-mode-styles");
|
||||
var rainbowModeStyles = document.getElementById("rainbow-mode-styles");
|
||||
var darkModeIcon = document.getElementById("dark-mode-icon");
|
||||
|
||||
if (toggleCount >= 18) {
|
||||
localStorage.setItem("dark-mode", "rainbow");
|
||||
lightModeStyles.disabled = true;
|
||||
darkModeStyles.disabled = true;
|
||||
rainbowModeStyles.disabled = false;
|
||||
darkModeIcon.src = "rainbow.svg";
|
||||
} else if (localStorage.getItem("dark-mode") == "on") {
|
||||
localStorage.setItem("dark-mode", "off");
|
||||
lightModeStyles.disabled = false;
|
||||
darkModeStyles.disabled = true;
|
||||
rainbowModeStyles.disabled = true;
|
||||
darkModeIcon.src = "sun.svg";
|
||||
} else {
|
||||
localStorage.setItem("dark-mode", "on");
|
||||
lightModeStyles.disabled = true;
|
||||
darkModeStyles.disabled = false;
|
||||
rainbowModeStyles.disabled = true;
|
||||
darkModeIcon.src = "moon.svg";
|
||||
|
@ -65,19 +74,23 @@ function toggleDarkMode() {
|
|||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
var lightModeStyles = document.getElementById("light-mode-styles");
|
||||
var darkModeStyles = document.getElementById("dark-mode-styles");
|
||||
var rainbowModeStyles = document.getElementById("rainbow-mode-styles");
|
||||
var darkModeIcon = document.getElementById("dark-mode-icon");
|
||||
|
||||
if (localStorage.getItem("dark-mode") == "on") {
|
||||
lightModeStyles.disabled = true;
|
||||
darkModeStyles.disabled = false;
|
||||
rainbowModeStyles.disabled = true;
|
||||
darkModeIcon.src = "moon.svg";
|
||||
} else if (localStorage.getItem("dark-mode") == "off") {
|
||||
lightModeStyles.disabled = false;
|
||||
darkModeStyles.disabled = true;
|
||||
rainbowModeStyles.disabled = true;
|
||||
darkModeIcon.src = "sun.svg";
|
||||
} else if (localStorage.getItem("dark-mode") == "rainbow") {
|
||||
lightModeStyles.disabled = true;
|
||||
darkModeStyles.disabled = true;
|
||||
rainbowModeStyles.disabled = false;
|
||||
darkModeIcon.src = "rainbow.svg";
|
||||
|
@ -101,79 +114,158 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
</script>
|
||||
|
||||
</head>
|
||||
<th:block th:fragment="fileSelector(name, multiple)" th:with="accept=${accept} ?: '*/*', inputText=${inputText} ?: #{pdfPrompt}">
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
|
||||
<th:block th:fragment="game">
|
||||
<dialog id="game-container-wrapper" class="game-container-wrapper" data-modal>
|
||||
<script>
|
||||
console.log("loaded game")
|
||||
$(document).ready(function() {
|
||||
|
||||
function loadGameScript(callback) {
|
||||
console.log('loadGameScript called');
|
||||
const script = document.createElement('script');
|
||||
script.src = 'js/game.js';
|
||||
script.onload = callback;
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
let gameScriptLoaded = false;
|
||||
$('#show-game-btn').on('click', function() {
|
||||
console.log('Show game button clicked');
|
||||
if (!gameScriptLoaded) {
|
||||
console.log('Show game button load');
|
||||
loadGameScript(function() {
|
||||
console.log('Game script loaded');
|
||||
window.initializeGame();
|
||||
gameScriptLoaded = true;
|
||||
});
|
||||
}
|
||||
$('#game-container-wrapper').show();
|
||||
});
|
||||
|
||||
|
||||
$('form').submit(function(event) {
|
||||
|
||||
const boredWaiting = localStorage.getItem('boredWaiting');
|
||||
if (boredWaiting === 'enabled') {
|
||||
$('#show-game-btn').show();
|
||||
function loadGameScript(callback) {
|
||||
console.log('loadGameScript called');
|
||||
const script = document.createElement('script');
|
||||
script.src = 'js/game.js';
|
||||
script.onload = callback;
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
let gameScriptLoaded = false;
|
||||
const gameDialog = document.getElementById('game-container-wrapper')
|
||||
$('#show-game-btn').on('click', function() {
|
||||
console.log('Show game button clicked');
|
||||
if (!gameScriptLoaded) {
|
||||
console.log('Show game button load');
|
||||
loadGameScript(function() {
|
||||
console.log('Game script loaded');
|
||||
window.initializeGame();
|
||||
gameScriptLoaded = true;
|
||||
});
|
||||
} else {
|
||||
window.resetGame();
|
||||
}
|
||||
var processing = "Processing..."
|
||||
var submitButtonText = $('#submitBtn').text()
|
||||
gameDialog.showModal();
|
||||
});
|
||||
gameDialog.addEventListener("click", e => {
|
||||
const dialogDimensions = gameDialog.getBoundingClientRect()
|
||||
if (
|
||||
e.clientX < dialogDimensions.left ||
|
||||
e.clientX > dialogDimensions.right ||
|
||||
e.clientY < dialogDimensions.top ||
|
||||
e.clientY > dialogDimensions.bottom
|
||||
) {
|
||||
gameDialog.close()
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<div id="game-container">
|
||||
<div id="lives">Lives: 3</div>
|
||||
<div id="score">Score: 0</div>
|
||||
<div id="high-score">High Score: 0</div>
|
||||
<div id="level">Level: 1</div>
|
||||
<img src="favicon.svg" class="player" id="player">
|
||||
</div>
|
||||
<style>
|
||||
#game-container {
|
||||
position: relative;
|
||||
width: 100vh;
|
||||
height: 0;
|
||||
padding-bottom: 75%; /* 4:3 aspect ratio */
|
||||
background-color: transparent;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
border: 2px solid black; /* Add border */
|
||||
}
|
||||
|
||||
.pdf, .player, .projectile {
|
||||
position: absolute;
|
||||
}
|
||||
.pdf {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
.player {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
.projectile {
|
||||
background-color: black !important;
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
}
|
||||
#score, #level, #lives, #high-score {
|
||||
color: black;
|
||||
font-family: sans-serif;
|
||||
position: absolute;
|
||||
font-size: calc(14px + 0.25vw); /* Reduced font size */
|
||||
}
|
||||
#score {
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
#lives {
|
||||
top: 10px;
|
||||
left: calc(7vw); /* Adjusted position */
|
||||
}
|
||||
#high-score {
|
||||
top: 10px;
|
||||
left: calc(14vw); /* Adjusted position */
|
||||
}
|
||||
#level {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</dialog>
|
||||
</th:block>
|
||||
|
||||
<th:block th:fragment="fileSelector(name, multiple)" th:with="accept=${accept} ?: '*/*', inputText=${inputText} ?: #{pdfPrompt}, remoteCall=${remoteCall} ?: 'true'">
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('form').submit(async function(event) {
|
||||
const boredWaiting = localStorage.getItem('boredWaiting');
|
||||
if (boredWaiting === 'enabled') {
|
||||
$('#show-game-btn').show();
|
||||
}
|
||||
var processing = "Processing..."
|
||||
var submitButtonText = $('#submitBtn').text()
|
||||
|
||||
$('#submitBtn').text('Processing...');
|
||||
console.log("start download code")
|
||||
var files = $('#fileInput-input')[0].files;
|
||||
var url = this.action;
|
||||
console.log(url)
|
||||
event.preventDefault(); // Prevent the default form handling behavior
|
||||
|
||||
/* Check if ${multiple} is false */
|
||||
var multiple = [[${multiple}]] || false;
|
||||
var override = $('#override').val() || '';
|
||||
console.log("override=" + override)
|
||||
|
||||
if([[${remoteCall}]] === true) {
|
||||
if (override === 'multi' || (!multiple && files.length > 1) && override !== 'single' ) {
|
||||
console.log("multi parallel download")
|
||||
await submitMultiPdfForm(event,url);
|
||||
} else {
|
||||
console.log("start single download")
|
||||
|
||||
// Get the selected download option from localStorage
|
||||
const downloadOption = localStorage.getItem('downloadOption');
|
||||
|
||||
var formData = new FormData($('form')[0]);
|
||||
|
||||
$('#submitBtn').text('Processing...');
|
||||
console.log("start download code")
|
||||
var files = $('#fileInput-input')[0].files;
|
||||
var url = this.action;
|
||||
console.log(url)
|
||||
event.preventDefault(); // Prevent the default form handling behavior
|
||||
/* Check if ${multiple} is false */
|
||||
var multiple = [[${multiple}]] || false;
|
||||
var override = $('#override').val() || '';
|
||||
console.log("override=" + override)
|
||||
if (override === 'multi' || (!multiple && files.length > 1) && override !== 'single' ) {
|
||||
console.log("multi parallel download")
|
||||
submitMultiPdfForm(event,url);
|
||||
} else {
|
||||
console.log("start single download")
|
||||
// Send the request to the server using the fetch() API
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
try {
|
||||
if (!response) {
|
||||
throw new Error('Received null response for file ' + i);
|
||||
}
|
||||
console.log("load single download")
|
||||
|
||||
// Get the selected download option from localStorage
|
||||
const downloadOption = localStorage.getItem('downloadOption');
|
||||
|
||||
var formData = new FormData($('form')[0]);
|
||||
|
||||
// Send the request to the server using the fetch() API
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(response => {
|
||||
if (!response) {
|
||||
throw new Error('Received null response for file ' + i);
|
||||
}
|
||||
console.log("load single download")
|
||||
|
||||
|
||||
// Extract the filename from the Content-Disposition header, if present
|
||||
|
||||
// Extract the filename from the Content-Disposition header, if present
|
||||
let filename = null;
|
||||
const contentDispositionHeader = response.headers.get('Content-Disposition');
|
||||
console.log(contentDispositionHeader)
|
||||
|
@ -188,369 +280,286 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
console.log("contentType=" + contentType)
|
||||
// Check if the response is a PDF or an image
|
||||
if (contentType.includes('pdf') || contentType.includes('image')) {
|
||||
response.blob().then(blob => {
|
||||
console.log("pdf/image")
|
||||
|
||||
// Perform the appropriate action based on the download option
|
||||
if (downloadOption === 'sameWindow') {
|
||||
console.log("same window")
|
||||
|
||||
// Open the file in the same window
|
||||
window.location.href = URL.createObjectURL(blob);
|
||||
} else if (downloadOption === 'newWindow') {
|
||||
console.log("new window")
|
||||
|
||||
// Open the file in a new window
|
||||
window.open(URL.createObjectURL(blob), '_blank');
|
||||
} else {
|
||||
console.log("else save")
|
||||
|
||||
// Download the file
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
}
|
||||
});
|
||||
} else if (contentType.includes('json')) {
|
||||
// Check if the response is a PDF or an image
|
||||
if (contentType.includes('pdf') || contentType.includes('image')) {
|
||||
const blob = await response.blob();
|
||||
console.log("pdf/image")
|
||||
|
||||
// Perform the appropriate action based on the download option
|
||||
if (downloadOption === 'sameWindow') {
|
||||
console.log("same window")
|
||||
|
||||
// Open the file in the same window
|
||||
window.location.href = URL.createObjectURL(blob);
|
||||
} else if (downloadOption === 'newWindow') {
|
||||
console.log("new window")
|
||||
|
||||
// Open the file in a new window
|
||||
window.open(URL.createObjectURL(blob), '_blank');
|
||||
} else {
|
||||
console.log("else save")
|
||||
|
||||
// Download the file
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
}
|
||||
} else if (contentType.includes('json')) {
|
||||
// Handle the JSON response
|
||||
response.json().then(data => {
|
||||
// Format the error message
|
||||
const errorMessage = JSON.stringify(data, null, 2);
|
||||
|
||||
// Display the error message in an alert
|
||||
alert(`An error occurred: ${errorMessage}`);
|
||||
});
|
||||
|
||||
} else {
|
||||
response.blob().then(blob => {
|
||||
console.log("else save 2 zip")
|
||||
|
||||
// For ZIP files or other file types, just download the file
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log("error listener")
|
||||
const json = await response.json();
|
||||
// Format the error message
|
||||
const errorMessage = JSON.stringify(json, null, 2);
|
||||
// Display the error message in an alert
|
||||
alert(`An error occurred: ${errorMessage}`);
|
||||
} else {
|
||||
const blob = await response.blob()
|
||||
console.log("else save 2 zip")
|
||||
|
||||
// For ZIP files or other file types, just download the file
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
}
|
||||
} catch(error) {
|
||||
console.log("error listener")
|
||||
|
||||
// Extract the error message and stack trace from the response
|
||||
const errorMessage = error.message;
|
||||
const stackTrace = error.stack;
|
||||
// Extract the error message and stack trace from the response
|
||||
const errorMessage = error.message;
|
||||
const stackTrace = error.stack;
|
||||
|
||||
// Create an error message to display to the user
|
||||
const message = `${errorMessage}\n\n${stackTrace}`;
|
||||
// Create an error message to display to the user
|
||||
const message = `${errorMessage}\n\n${stackTrace}`;
|
||||
|
||||
$('#submitBtn').text(submitButtonText);
|
||||
|
||||
// Display the error message to the user
|
||||
alert(message);
|
||||
|
||||
};
|
||||
|
||||
$('#submitBtn').text(submitButtonText);
|
||||
|
||||
// Display the error message to the user
|
||||
alert(message);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
$('#submitBtn').text(submitButtonText);
|
||||
});
|
||||
});
|
||||
async function submitMultiPdfForm(event, url) {
|
||||
// Get the selected PDF files
|
||||
let files = $('#fileInput-input')[0].files;
|
||||
|
||||
// Get the existing form data
|
||||
let formData = new FormData($('form')[0]);
|
||||
formData.delete('fileInput');
|
||||
|
||||
// Show the progress bar
|
||||
$('#progressBarContainer').show();
|
||||
|
||||
// Initialize the progress bar
|
||||
let progressBar = $('#progressBar');
|
||||
progressBar.css('width', '0%');
|
||||
progressBar.attr('aria-valuenow', 0);
|
||||
progressBar.attr('aria-valuemax', files.length);
|
||||
|
||||
// Check the flag in localStorage, default to 4
|
||||
const zipThreshold = parseInt(localStorage.getItem('zipThreshold'), 10) || 4;
|
||||
const zipFiles = files.length > zipThreshold;
|
||||
|
||||
// Initialize JSZip instance if needed
|
||||
let jszip = null;
|
||||
if (zipFiles) {
|
||||
jszip = new JSZip();
|
||||
}
|
||||
|
||||
// Submit each PDF file in parallel
|
||||
let promises = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let promise = new Promise(async function(resolve, reject) {
|
||||
let fileFormData = new FormData();
|
||||
fileFormData.append('fileInput', files[i]);
|
||||
for (let pair of formData.entries()) {
|
||||
fileFormData.append(pair[0], pair[1]);
|
||||
}
|
||||
console.log(fileFormData);
|
||||
|
||||
try {
|
||||
let response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: fileFormData
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
throw new Error('Received null response for file ' + i);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error submitting request for file ${i}: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
let contentDisposition = response.headers.get('content-disposition');
|
||||
let fileName = "file.pdf"
|
||||
if (!contentDisposition) {
|
||||
//throw new Error('Content-Disposition header not found for file ' + i);
|
||||
} else {
|
||||
fileName = contentDisposition.split('filename=')[1].replace(/"/g, '');
|
||||
}
|
||||
console.log('Received response for file ' + i + ': ' + response);
|
||||
|
||||
|
||||
|
||||
let blob = await response.blob();
|
||||
if (zipFiles) {
|
||||
// Add the file to the ZIP archive
|
||||
jszip.file(fileName, blob);
|
||||
resolve();
|
||||
} else {
|
||||
// Download the file directly
|
||||
let url = window.URL.createObjectURL(blob);
|
||||
let a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
resolve();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error submitting request for file ' + i + ': ' + error);
|
||||
|
||||
// Set default values or fallbacks for error properties
|
||||
let status = error && error.status || 500;
|
||||
let statusText = error && error.statusText || 'Internal Server Error';
|
||||
let message = error && error.message || 'An error occurred while processing your request.';
|
||||
|
||||
// Reject the Promise to signal that the request has failed
|
||||
reject();
|
||||
// Redirect to error page with Spring Boot error parameters
|
||||
let url = '/error?status=' + status + '&error=' + encodeURIComponent(statusText) + '&message=' + encodeURIComponent(message);
|
||||
window.location.href = url;
|
||||
}
|
||||
});
|
||||
|
||||
// Update the progress bar as each request finishes
|
||||
promise.then(function() {
|
||||
updateProgressBar(progressBar, files);
|
||||
});
|
||||
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
// Wait for all requests to finish
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
console.error('Error while uploading files: ' + error);
|
||||
}
|
||||
|
||||
// Update the progress bar
|
||||
progressBar.css('width', '100%');
|
||||
progressBar.attr('aria-valuenow', files.length);
|
||||
|
||||
// After all requests are finished, download the ZIP file if needed
|
||||
if (zipFiles) {
|
||||
try {
|
||||
let content = await jszip.generateAsync({ type: "blob" });
|
||||
let url = window.URL.createObjectURL(content);
|
||||
let a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = "files.zip";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
} catch (error) {
|
||||
console.error('Error generating ZIP file: ' + error);
|
||||
}
|
||||
} else {
|
||||
//Offline do nothing and let other scripts handle everything
|
||||
|
||||
}
|
||||
$('#submitBtn').text(submitButtonText);
|
||||
});
|
||||
});
|
||||
async function submitMultiPdfForm(event, url) {
|
||||
// Get the selected PDF files
|
||||
let files = $('#fileInput-input')[0].files;
|
||||
|
||||
// Get the existing form data
|
||||
let formData = new FormData($('form')[0]);
|
||||
formData.delete('fileInput');
|
||||
|
||||
// Show the progress bar
|
||||
$('#progressBarContainer').show();
|
||||
|
||||
// Initialize the progress bar
|
||||
let progressBar = $('#progressBar');
|
||||
progressBar.css('width', '0%');
|
||||
progressBar.attr('aria-valuenow', 0);
|
||||
progressBar.attr('aria-valuemax', files.length);
|
||||
|
||||
// Check the flag in localStorage, default to 4
|
||||
const zipThreshold = parseInt(localStorage.getItem('zipThreshold'), 10) || 4;
|
||||
const zipFiles = files.length > zipThreshold;
|
||||
|
||||
// Initialize JSZip instance if needed
|
||||
let jszip = null;
|
||||
if (zipFiles) {
|
||||
jszip = new JSZip();
|
||||
}
|
||||
|
||||
// Submit each PDF file in parallel
|
||||
let promises = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
let promise = new Promise(async function(resolve, reject) {
|
||||
let fileFormData = new FormData();
|
||||
fileFormData.append('fileInput', files[i]);
|
||||
for (let pair of formData.entries()) {
|
||||
fileFormData.append(pair[0], pair[1]);
|
||||
}
|
||||
console.log(fileFormData);
|
||||
|
||||
try {
|
||||
let response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: fileFormData
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
throw new Error('Received null response for file ' + i);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error submitting request for file ${i}: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
let contentDisposition = response.headers.get('content-disposition');
|
||||
let fileName = "file.pdf"
|
||||
if (!contentDisposition) {
|
||||
//throw new Error('Content-Disposition header not found for file ' + i);
|
||||
} else {
|
||||
fileName = contentDisposition.split('filename=')[1].replace(/"/g, '');
|
||||
}
|
||||
console.log('Received response for file ' + i + ': ' + response);
|
||||
|
||||
|
||||
|
||||
let blob = await response.blob();
|
||||
if (zipFiles) {
|
||||
// Add the file to the ZIP archive
|
||||
jszip.file(fileName, blob);
|
||||
resolve();
|
||||
} else {
|
||||
// Download the file directly
|
||||
let url = window.URL.createObjectURL(blob);
|
||||
let a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
resolve();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error submitting request for file ' + i + ': ' + error);
|
||||
|
||||
// Set default values or fallbacks for error properties
|
||||
let status = error && error.status || 500;
|
||||
let statusText = error && error.statusText || 'Internal Server Error';
|
||||
let message = error && error.message || 'An error occurred while processing your request.';
|
||||
|
||||
// Reject the Promise to signal that the request has failed
|
||||
reject();
|
||||
// Redirect to error page with Spring Boot error parameters
|
||||
let url = '/error?status=' + status + '&error=' + encodeURIComponent(statusText) + '&message=' + encodeURIComponent(message);
|
||||
window.location.href = url;
|
||||
}
|
||||
});
|
||||
|
||||
// Update the progress bar as each request finishes
|
||||
promise.then(function() {
|
||||
updateProgressBar(progressBar, files);
|
||||
});
|
||||
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
// Wait for all requests to finish
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
console.error('Error while uploading files: ' + error);
|
||||
}
|
||||
|
||||
// Update the progress bar
|
||||
progressBar.css('width', '100%');
|
||||
progressBar.attr('aria-valuenow', files.length);
|
||||
|
||||
// After all requests are finished, download the ZIP file if needed
|
||||
if (zipFiles) {
|
||||
try {
|
||||
let content = await jszip.generateAsync({ type: "blob" });
|
||||
let url = window.URL.createObjectURL(content);
|
||||
let a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = "files.zip";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
} catch (error) {
|
||||
console.error('Error generating ZIP file: ' + error);
|
||||
}
|
||||
}
|
||||
function updateProgressBar(progressBar, files) {
|
||||
let progress = ((progressBar.attr('aria-valuenow') / files.length) * 100) + (100 / files.length);
|
||||
progressBar.css('width', progress + '%');
|
||||
progressBar.attr('aria-valuenow', parseInt(progressBar.attr('aria-valuenow')) + 1);
|
||||
}
|
||||
}
|
||||
function updateProgressBar(progressBar, files) {
|
||||
let progress = ((progressBar.attr('aria-valuenow') / files.length) * 100) + (100 / files.length);
|
||||
progressBar.css('width', progress + '%');
|
||||
progressBar.attr('aria-valuenow', parseInt(progressBar.attr('aria-valuenow')) + 1);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="custom-file-chooser">
|
||||
<div class="custom-file">
|
||||
<input type="file" class="custom-file-input" th:name="${name}" th:id="${name}+'-input'" th:accept="${accept}" multiple>
|
||||
<label class="custom-file-label" th:for="${name}+'-input'" th:text="${inputText}"></label>
|
||||
</div>
|
||||
<div class="selected-files"></div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="custom-file">
|
||||
<input type="file" class="custom-file-input" th:name="${name}" th:id="${name}+'-input'" th:accept="${accept}" multiple required>
|
||||
<label class="custom-file-label" th:for="${name}+'-input'" th:text="${inputText}"></label>
|
||||
</div>
|
||||
<div class="selected-files"></div>
|
||||
</div>
|
||||
<div id="progressBarContainer" style="display: none; position: relative;">
|
||||
<div class="progress" style="height: 1rem;">
|
||||
<div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress" style="height: 1rem;">
|
||||
<div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" id="show-game-btn" style="display:none;">Bored waiting?</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" id="show-game-btn" style="display:none;">Bored waiting?</button>
|
||||
|
||||
<div id="game-container-wrapper" class="game-container-wrapper" style="display: none;">
|
||||
<div id="game-container">
|
||||
<div id="lives">Lives: 3</div>
|
||||
<div id="score">Score: 0</div>
|
||||
<div id="high-score">High Score: 0</div>
|
||||
<div id="level">Level: 1</div>
|
||||
<img src="favicon.svg" class="player" id="player">
|
||||
</div>
|
||||
<style>
|
||||
#game-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
padding-bottom: 75%; /* 4:3 aspect ratio */
|
||||
background-color: transparent;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
border: 2px solid black; /* Add border */
|
||||
}
|
||||
|
||||
.pdf, .player, .projectile {
|
||||
position: absolute;
|
||||
}
|
||||
.pdf {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
.player {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
.projectile {
|
||||
background-color: black !important;
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
}
|
||||
#score, #level, #lives, #high-score {
|
||||
color: black;
|
||||
font-family: sans-serif;
|
||||
position: absolute;
|
||||
font-size: calc(14px + 0.25vw); /* Reduced font size */
|
||||
}
|
||||
#score {
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
#lives {
|
||||
top: 10px;
|
||||
left: calc(7vw); /* Adjusted position */
|
||||
}
|
||||
#high-score {
|
||||
top: 10px;
|
||||
left: calc(14vw); /* Adjusted position */
|
||||
}
|
||||
#level {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<script th:inline="javascript">
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('fileInput-input');
|
||||
|
||||
// Prevent default behavior for drag events
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
fileInput.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById([[${name+"-input"}]]);
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
// Prevent default behavior for drag events
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
fileInput.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
// Add drop event listener
|
||||
fileInput.addEventListener('drop', handleDrop, false);
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function handleDrop(e) {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
fileInput.files = files;
|
||||
handleFileInputChange(fileInput)
|
||||
}
|
||||
// Add drop event listener
|
||||
fileInput.addEventListener('drop', handleDrop, false);
|
||||
|
||||
function handleDrop(e) {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
fileInput.files = files;
|
||||
handleFileInputChange(fileInput)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$([[${"#"+name+"-input"}]]).on("change", function() {
|
||||
$([[${"#"+name+"-input"}]]).on("change", function() {
|
||||
handleFileInputChange(this);
|
||||
});
|
||||
|
||||
function handleFileInputChange(inputElement) {
|
||||
const files = $(inputElement).get(0).files;
|
||||
const fileNames = Array.from(files).map(f => f.name);
|
||||
const selectedFilesContainer = $(inputElement).siblings(".selected-files");
|
||||
selectedFilesContainer.empty();
|
||||
fileNames.forEach(fileName => {
|
||||
selectedFilesContainer.append("<div>" + fileName + "</div>");
|
||||
});
|
||||
console.log("fileNames.length=" + fileNames.length)
|
||||
if (fileNames.length === 1) {
|
||||
$(inputElement).siblings(".custom-file-label").addClass("selected").html(fileNames[0]);
|
||||
} else if (fileNames.length > 1) {
|
||||
$(inputElement).siblings(".custom-file-label").addClass("selected").html(fileNames.length + " files selected");
|
||||
} else {
|
||||
$(inputElement).siblings(".custom-file-label").addClass("selected").html([[#{pdfPrompt}]]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function handleFileInputChange(inputElement) {
|
||||
const files = $(inputElement).get(0).files;
|
||||
const fileNames = Array.from(files).map(f => f.name);
|
||||
const selectedFilesContainer = $(inputElement).siblings(".selected-files");
|
||||
selectedFilesContainer.empty();
|
||||
fileNames.forEach(fileName => {
|
||||
selectedFilesContainer.append("<div>" + fileName + "</div>");
|
||||
});
|
||||
console.log("fileNames.length=" + fileNames.length)
|
||||
if (fileNames.length === 1) {
|
||||
$(inputElement).siblings(".custom-file-label").addClass("selected").html(fileNames[0]);
|
||||
} else if (fileNames.length > 1) {
|
||||
$(inputElement).siblings(".custom-file-label").addClass("selected").html(fileNames.length + " files selected");
|
||||
} else {
|
||||
$(inputElement).siblings(".custom-file-label").addClass("selected").html([[#{pdfPrompt}]]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.custom-file-label {
|
||||
padding-right: 90px;
|
||||
}
|
||||
|
||||
.selected-files {
|
||||
margin-top: 10px;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.custom-file-label {
|
||||
padding-right: 90px;
|
||||
}
|
||||
.selected-files {
|
||||
margin-top: 10px;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
</th:block>
|
|
@ -259,7 +259,7 @@ function compareVersions(version1, version2) {
|
|||
</li>
|
||||
|
||||
<li class="nav-item nav-item-separator"></li>
|
||||
<li class="nav-item dropdown" th:classappend="${currentPage}=='add-image' OR ${currentPage}=='ocr-pdf' OR ${currentPage}=='change-permissions' OR ${currentPage}=='extract-images' OR ${currentPage}=='compress-pdf' ? 'active' : ''">
|
||||
<li class="nav-item dropdown" th:classappend="${currentPage}=='sign' OR ${currentPage}=='flatten' OR ${currentPage}=='add-image' OR ${currentPage}=='ocr-pdf' OR ${currentPage}=='change-permissions' OR ${currentPage}=='extract-images' OR ${currentPage}=='compress-pdf' ? 'active' : ''">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<img class="icon" src="images/card-list.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;">
|
||||
<span class="icon-text" th:text="#{navbar.other}"></span>
|
||||
|
@ -283,13 +283,37 @@ function compareVersions(version1, version2) {
|
|||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{extract-image-scans}" th:classappend="${currentPage}=='extract-image-scans' ? 'active' : ''" th:title="#{home.ScannerImageSplit.desc}">
|
||||
<img class="icon" src="images/scanner.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.ScannerImageSplit.title}"></span>
|
||||
</a>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{sign}" th:classappend="${currentPage}=='sign' ? 'active' : ''" th:title="#{home.sign.desc}">
|
||||
<img class="icon" src="images/sign.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.sign.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{flatten}" th:classappend="${currentPage}=='flatten' ? 'active' : ''" th:title="#{home.flatten.desc}">
|
||||
<img class="icon" src="images/flatten.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.flatten.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{repair}" th:classappend="${currentPage}=='repair' ? 'active' : ''" th:title="#{home.repair.desc}">
|
||||
<img class="icon" src="images/wrench.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.repair.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{remove-blanks}" th:classappend="${currentPage}=='remove-blanks' ? 'active' : ''" th:title="#{home.removeBlanks.desc}">
|
||||
<img class="icon" src="images/blank-file.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.removeBlanks.title}"></span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" th:href="@{compare}" th:classappend="${currentPage}=='compare' ? 'active' : ''" th:title="#{home.compare.desc}">
|
||||
<img class="icon" src="images/scales.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.compare.title}"></span>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
<ul class="navbar-nav ml-auto flex-nowrap">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<img class="navbar-icon" src="images/star.svg" alt="icon" width="24" height="24">
|
||||
</a>
|
||||
<div class="dropdown-menu" id="favoritesDropdown" aria-labelledby="navbarDropdown">
|
||||
<!-- Dropdown items will be added here by JavaScript -->
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="dark-mode-toggle" href="#">
|
||||
<img class="navbar-icon" id="dark-mode-icon" src="moon.svg" alt="icon" />
|
||||
|
@ -322,7 +346,7 @@ function compareVersions(version1, version2) {
|
|||
<img src="images/flags/cn.svg" alt="icon" width="20" height="15"> 简体中文
|
||||
</a>
|
||||
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="ca_CA">
|
||||
<img src="images/flags/ca.svg" alt="icon" width="20" height="15"> Català
|
||||
<img src="images/flags/es-ct.svg" alt="icon" width="20" height="15"> Català
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -340,6 +364,48 @@ function compareVersions(version1, version2) {
|
|||
</div>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
|
||||
|
||||
function updateFavoritesDropdown() {
|
||||
var dropdown = document.querySelector('#favoritesDropdown');
|
||||
dropdown.innerHTML = ''; // Clear the current favorites
|
||||
|
||||
|
||||
|
||||
var hasFavorites = false;
|
||||
|
||||
for (var i = 0; i < localStorage.length; i++) {
|
||||
var key = localStorage.key(i);
|
||||
if (localStorage.getItem(key) === 'favorite') {
|
||||
// Find the corresponding navbar entry
|
||||
var navbarEntry = document.querySelector(`a[href='${key}']`);
|
||||
if (navbarEntry) {
|
||||
// Create a new dropdown entry
|
||||
var dropdownItem = document.createElement('a');
|
||||
dropdownItem.className = 'dropdown-item';
|
||||
dropdownItem.href = navbarEntry.href;
|
||||
dropdownItem.innerHTML = navbarEntry.innerHTML;
|
||||
dropdown.appendChild(dropdownItem);
|
||||
hasFavorites = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show or hide the default item based on whether there are any favorites
|
||||
if(!hasFavorites){
|
||||
var defaultItem = document.createElement('a');
|
||||
defaultItem.className = 'dropdown-item';
|
||||
defaultItem.textContent = 'No favourites added';
|
||||
dropdown.appendChild(defaultItem);
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
updateFavoritesDropdown();
|
||||
});
|
||||
</script>
|
||||
|
||||
</nav>
|
||||
<div th:insert="~{fragments/errorBanner.html :: errorBanner}"></div>
|
||||
|
||||
|
@ -389,6 +455,8 @@ function compareVersions(version1, version2) {
|
|||
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
// Get the download option from local storage, or set it to 'sameWindow' if it doesn't exist
|
||||
var downloadOption = localStorage.getItem('downloadOption')
|
||||
|| 'sameWindow';
|
||||
|
|
|
@ -57,7 +57,15 @@
|
|||
filter: invert(0.2) sepia(2) saturate(50) hue-rotate(190deg);
|
||||
}
|
||||
|
||||
|
||||
.favorite-icon {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.favorite-icon img {
|
||||
filter: brightness(0);
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
|
@ -75,42 +83,99 @@ filter: invert(0.2) sepia(2) saturate(50) hue-rotate(190deg);
|
|||
|
||||
<!-- Features -->
|
||||
<div class="features-container container">
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', svgPath='images/tools.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', svgPath='images/union.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.split.title}, cardText=#{home.split.desc}, cardLink='split-pdfs', svgPath='images/layout-split.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', svgPath='images/tools.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='merge-pdfs', cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', svgPath='images/union.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='split-pdfs', cardTitle=#{home.split.title}, cardText=#{home.split.desc}, cardLink='split-pdfs', svgPath='images/layout-split.svg')}"></div>
|
||||
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.rotate.title}, cardText=#{home.rotate.desc}, cardLink='rotate-pdf', svgPath='images/arrow-clockwise.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.imageToPdf.title}, cardText=#{home.imageToPdf.desc}, cardLink='img-to-pdf', svgPath='images/image.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.pdfToImage.title}, cardText=#{home.pdfToImage.desc}, cardLink='pdf-to-img', svgPath='images/image.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='rotate-pdf', cardTitle=#{home.rotate.title}, cardText=#{home.rotate.desc}, cardLink='rotate-pdf', svgPath='images/arrow-clockwise.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='img-to-pdf', cardTitle=#{home.imageToPdf.title}, cardText=#{home.imageToPdf.desc}, cardLink='img-to-pdf', svgPath='images/image.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='pdf-to-img', cardTitle=#{home.pdfToImage.title}, cardText=#{home.pdfToImage.desc}, cardLink='pdf-to-img', svgPath='images/image.svg')}"></div>
|
||||
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.pdfOrganiser.title}, cardText=#{home.pdfOrganiser.desc}, cardLink='pdf-organizer', svgPath='images/sort-numeric-down.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.addImage.title}, cardText=#{home.addImage.desc}, cardLink='add-image', svgPath='images/file-earmark-richtext.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.watermark.title}, cardText=#{home.watermark.desc}, cardLink='add-watermark', svgPath='images/droplet.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='pdf-organizer', cardTitle=#{home.pdfOrganiser.title}, cardText=#{home.pdfOrganiser.desc}, cardLink='pdf-organizer', svgPath='images/sort-numeric-down.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='add-image', cardTitle=#{home.addImage.title}, cardText=#{home.addImage.desc}, cardLink='add-image', svgPath='images/file-earmark-richtext.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='add-watermark', cardTitle=#{home.watermark.title}, cardText=#{home.watermark.desc}, cardLink='add-watermark', svgPath='images/droplet.svg')}"></div>
|
||||
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.fileToPDF.title}, cardText=#{home.fileToPDF.desc}, cardLink='file-to-pdf', svgPath='images/file.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.removePages.title}, cardText=#{home.removePages.desc}, cardLink='remove-pages', svgPath='images/file-earmark-x.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.addPassword.title}, cardText=#{home.addPassword.desc}, cardLink='add-password', svgPath='images/lock.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='file-to-pdf', cardTitle=#{home.fileToPDF.title}, cardText=#{home.fileToPDF.desc}, cardLink='file-to-pdf', svgPath='images/file.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='remove-pages', cardTitle=#{home.removePages.title}, cardText=#{home.removePages.desc}, cardLink='remove-pages', svgPath='images/file-earmark-x.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='add-password', cardTitle=#{home.addPassword.title}, cardText=#{home.addPassword.desc}, cardLink='add-password', svgPath='images/lock.svg')}"></div>
|
||||
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.removePassword.title}, cardText=#{home.removePassword.desc}, cardLink='remove-password', svgPath='images/unlock.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.compressPdfs.title}, cardText=#{home.compressPdfs.desc}, cardLink='compress-pdf', svgPath='images/file-zip.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.changeMetadata.title}, cardText=#{home.changeMetadata.desc}, cardLink='change-metadata', svgPath='images/clipboard-data.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='remove-password', cardTitle=#{home.removePassword.title}, cardText=#{home.removePassword.desc}, cardLink='remove-password', svgPath='images/unlock.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='compress-pdf', cardTitle=#{home.compressPdfs.title}, cardText=#{home.compressPdfs.desc}, cardLink='compress-pdf', svgPath='images/file-zip.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='change-metadata', cardTitle=#{home.changeMetadata.title}, cardText=#{home.changeMetadata.desc}, cardLink='change-metadata', svgPath='images/clipboard-data.svg')}"></div>
|
||||
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.permissions.title}, cardText=#{home.permissions.desc}, cardLink='change-permissions', svgPath='images/shield-lock.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.ocr.title}, cardText=#{home.ocr.desc}, cardLink='ocr-pdf', svgPath='images/search.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.extractImages.title}, cardText=#{home.extractImages.desc}, cardLink='extract-images', svgPath='images/images.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='change-permissions', cardTitle=#{home.permissions.title}, cardText=#{home.permissions.desc}, cardLink='change-permissions', svgPath='images/shield-lock.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='ocr-pdf', cardTitle=#{home.ocr.title}, cardText=#{home.ocr.desc}, cardLink='ocr-pdf', svgPath='images/search.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='extract-images', cardTitle=#{home.extractImages.title}, cardText=#{home.extractImages.desc}, cardLink='extract-images', svgPath='images/images.svg')}"></div>
|
||||
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.pdfToPDFA.title}, cardText=#{home.pdfToPDFA.desc}, cardLink='pdf-to-pdfa', svgPath='images/file-earmark-pdf.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.PDFToWord.title}, cardText=#{home.PDFToWord.desc}, cardLink='pdf-to-word', svgPath='images/file-earmark-word.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.PDFToPresentation.title}, cardText=#{home.PDFToPresentation.desc}, cardLink='pdf-to-presentation', svgPath='images/file-earmark-ppt.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='pdf-to-pdfa', cardTitle=#{home.pdfToPDFA.title}, cardText=#{home.pdfToPDFA.desc}, cardLink='pdf-to-pdfa', svgPath='images/file-earmark-pdf.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='pdf-to-word', cardTitle=#{home.PDFToWord.title}, cardText=#{home.PDFToWord.desc}, cardLink='pdf-to-word', svgPath='images/file-earmark-word.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='pdf-to-presentation', cardTitle=#{home.PDFToPresentation.title}, cardText=#{home.PDFToPresentation.desc}, cardLink='pdf-to-presentation', svgPath='images/file-earmark-ppt.svg')}"></div>
|
||||
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.PDFToText.title}, cardText=#{home.PDFToText.desc}, cardLink='pdf-to-text', svgPath='images/filetype-txt.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.PDFToHTML.title}, cardText=#{home.PDFToHTML.desc}, cardLink='pdf-to-html', svgPath='images/filetype-html.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.PDFToXML.title}, cardText=#{home.PDFToXML.desc}, cardLink='pdf-to-xml', svgPath='images/filetype-xml.svg')}"></div>
|
||||
|
||||
<div th:replace="~{fragments/card :: card(cardTitle=#{home.ScannerImageSplit.title}, cardText=#{home.ScannerImageSplit.desc}, cardLink='extract-image-scans', svgPath='images/scanner.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='pdf-to-text', cardTitle=#{home.PDFToText.title}, cardText=#{home.PDFToText.desc}, cardLink='pdf-to-text', svgPath='images/filetype-txt.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='pdf-to-html', cardTitle=#{home.PDFToHTML.title}, cardText=#{home.PDFToHTML.desc}, cardLink='pdf-to-html', svgPath='images/filetype-html.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='pdf-to-xml', cardTitle=#{home.PDFToXML.title}, cardText=#{home.PDFToXML.desc}, cardLink='pdf-to-xml', svgPath='images/filetype-xml.svg')}"></div>
|
||||
|
||||
<div th:replace="~{fragments/card :: card(id='extract-image-scans', cardTitle=#{home.ScannerImageSplit.title}, cardText=#{home.ScannerImageSplit.desc}, cardLink='extract-image-scans', svgPath='images/scanner.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='sign', cardTitle=#{home.sign.title}, cardText=#{home.sign.desc}, cardLink='sign', svgPath='images/sign.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='flatten', cardTitle=#{home.flatten.title}, cardText=#{home.flatten.desc}, cardLink='flatten', svgPath='images/flatten.svg')}"></div>
|
||||
|
||||
<div th:replace="~{fragments/card :: card(id='repair', cardTitle=#{home.repair.title}, cardText=#{home.repair.desc}, cardLink='repair', svgPath='images/wrench.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='remove-blanks', cardTitle=#{home.removeBlanks.title}, cardText=#{home.removeBlanks.desc}, cardLink='remove-blanks', svgPath='images/blank-file.svg')}"></div>
|
||||
<div th:replace="~{fragments/card :: card(id='compare', cardTitle=#{home.compare.title}, cardText=#{home.compare.desc}, cardLink='compare', svgPath='images/scales.svg')}"></div>
|
||||
|
||||
<script>
|
||||
function toggleFavorite(element) {
|
||||
var img = element.querySelector('img');
|
||||
var card = element.closest('.feature-card');
|
||||
var cardId = card.id;
|
||||
if (img.src.endsWith('star.svg')) {
|
||||
img.src = 'images/star-fill.svg';
|
||||
card.classList.add('favorite');
|
||||
localStorage.setItem(cardId, 'favorite');
|
||||
} else {
|
||||
img.src = 'images/star.svg';
|
||||
card.classList.remove('favorite');
|
||||
localStorage.removeItem(cardId);
|
||||
}
|
||||
reorderCards();
|
||||
updateFavoritesDropdown();
|
||||
}
|
||||
|
||||
function reorderCards() {
|
||||
var container = document.querySelector('.features-container');
|
||||
var cards = Array.from(container.getElementsByClassName('feature-card'));
|
||||
cards.sort(function(a, b) {
|
||||
var aIsFavorite = localStorage.getItem(a.id) === 'favorite';
|
||||
var bIsFavorite = localStorage.getItem(b.id) === 'favorite';
|
||||
if (aIsFavorite && !bIsFavorite) {
|
||||
return -1;
|
||||
}
|
||||
if (!aIsFavorite && bIsFavorite) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
cards.forEach(function(card) {
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
function initializeCards() {
|
||||
var cards = document.querySelectorAll('.feature-card');
|
||||
cards.forEach(function(card) {
|
||||
var cardId = card.id;
|
||||
var img = card.querySelector('.favorite-icon img');
|
||||
if (localStorage.getItem(cardId) === 'favorite') {
|
||||
img.src = 'images/star-fill.svg';
|
||||
card.classList.add('favorite');
|
||||
}
|
||||
});
|
||||
reorderCards();
|
||||
updateFavoritesDropdown();
|
||||
}
|
||||
window.onload = initializeCards;
|
||||
|
||||
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
|
||||
|
||||
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{addImage.title})}"></th:block>
|
||||
<html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{addImage.title})}"></th:block>
|
||||
<script src="js/interact.min.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
|
@ -14,23 +15,121 @@
|
|||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h2 th:text="#{addImage.header}"></h2>
|
||||
<form method="post" th:action="@{add-image}" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
|
||||
<div class="custom-file">
|
||||
<input type="file" class="custom-file-input" id="fileInput2" name="fileInput2" accept="image/*" required>
|
||||
<label class="custom-file-label" for="fileInput2" th:text="#{imgPrompt}"></label>
|
||||
|
||||
<!-- pdf selector -->
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multiple=false, accept='application/pdf')}"></div>
|
||||
<script>
|
||||
let originalFileName = '';
|
||||
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
originalFileName = file.name.replace(/\.[^/.]+$/, "");
|
||||
const pdfData = await file.arrayBuffer();
|
||||
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
await DraggableUtils.renderPage(pdfDoc, 0);
|
||||
|
||||
document.querySelectorAll(".show-on-file-selected").forEach(el => {
|
||||
el.style.cssText = '';
|
||||
})
|
||||
}
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.querySelectorAll(".show-on-file-selected").forEach(el => {
|
||||
el.style.cssText = "display:none !important";
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="tab-group show-on-file-selected">
|
||||
<div class="tab-container" th:title="#{addImage.upload}">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='image-upload', multiple=true, accept='image/*', inputText=#{imgPrompt})}"></div>
|
||||
<script>
|
||||
const imageUpload = document.querySelector('input[name=image-upload]');
|
||||
imageUpload.addEventListener('change', e => {
|
||||
if(!e.target.files) {
|
||||
return;
|
||||
}
|
||||
for (const imageFile of e.target.files) {
|
||||
var reader = new FileReader();
|
||||
reader.readAsDataURL(imageFile);
|
||||
reader.onloadend = function (e) {
|
||||
DraggableUtils.createDraggableCanvasFromUrl(e.target.result);
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- draggables box -->
|
||||
<div id="box-drag-container" class="show-on-file-selected">
|
||||
<canvas id="pdf-canvas"></canvas>
|
||||
<script src="js/draggable-utils.js"></script>
|
||||
<div class="draggable-buttons-box ignore-rtl">
|
||||
<button class="btn btn-outline-secondary" onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/>
|
||||
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('lang-direction')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()" style="margin-left:auto">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-left" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('lang-direction')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-right" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="x">X</label> <input type="number" class="form-control" id="x" name="x" step="0.01" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="y">Y</label> <input type="number" class="form-control" id="y" name="y" step="0.01" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="everyPage" name="everyPage" value="true"> <label for="everyPage" th:text="#{addImage.everyPage}"></label>
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{addImage.submit}"></button>
|
||||
</form>
|
||||
<style>
|
||||
#box-drag-container {
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
}
|
||||
#pdf-canvas {
|
||||
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
|
||||
width: 100%;
|
||||
}
|
||||
.draggable-buttons-box {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
.draggable-buttons-box > button {
|
||||
z-index: 10;
|
||||
background-color: rgba(13, 110, 253, 0.1);
|
||||
}
|
||||
.draggable-canvas {
|
||||
border: 1px solid red;
|
||||
position: absolute;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
top: 0px;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
|
||||
<!-- download button -->
|
||||
<div class="margin-auto-parent">
|
||||
<button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center">Download PDF</button>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById("download-pdf").addEventListener('click', async() => {
|
||||
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
|
||||
const modifiedPdfBytes = await modifiedPdf.save();
|
||||
const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = originalFileName + '_addedImage.pdf';
|
||||
link.click();
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -38,5 +137,4 @@
|
|||
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
190
src/main/resources/templates/other/compare.html
Normal file
|
@ -0,0 +1,190 @@
|
|||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
|
||||
|
||||
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{compare.title})}"></th:block>
|
||||
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
<br> <br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-9">
|
||||
<h2 th:text="#{compare.header}"></h2>
|
||||
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput2', multiple=false, accept='application/pdf')}"></div>
|
||||
<button class="btn btn-primary" onclick="comparePDFs()" th:text="#{compare.submit}"></button>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3 th:text="#{compare.document.1}"></h3>
|
||||
<div id="result1" class="result-column"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h3 th:text="#{compare.document.2}"></h3>
|
||||
<div id="result2" class="result-column"></div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.result-column {
|
||||
border: 1px solid #ccc;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - 400px);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
// get the elements
|
||||
var result1 = document.getElementById('result1');
|
||||
var result2 = document.getElementById('result2');
|
||||
|
||||
// add event listeners
|
||||
result1.addEventListener('scroll', function() {
|
||||
result2.scrollTop = result1.scrollTop;
|
||||
});
|
||||
|
||||
result2.addEventListener('scroll', function() {
|
||||
result1.scrollTop = result2.scrollTop;
|
||||
});
|
||||
|
||||
async function comparePDFs() {
|
||||
const file1 = document.getElementById("fileInput-input").files[0];
|
||||
const file2 = document.getElementById("fileInput2-input").files[0];
|
||||
|
||||
if (!file1 || !file2) {
|
||||
console.error("Please select two PDF files to compare");
|
||||
return;
|
||||
}
|
||||
|
||||
const [pdf1, pdf2] = await Promise.all([
|
||||
pdfjsLib.getDocument(URL.createObjectURL(file1)).promise,
|
||||
pdfjsLib.getDocument(URL.createObjectURL(file2)).promise
|
||||
]);
|
||||
|
||||
const extractText = async (pdf) => {
|
||||
const pages = [];
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const content = await page.getTextContent();
|
||||
const strings = content.items.map(item => item.str);
|
||||
pages.push(strings.join(" "));
|
||||
}
|
||||
return pages.join(" ");
|
||||
};
|
||||
|
||||
const [text1, text2] = await Promise.all([
|
||||
extractText(pdf1),
|
||||
extractText(pdf2)
|
||||
]);
|
||||
|
||||
if (text1.trim() === "" || text2.trim() === "") {
|
||||
alert("One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison.");
|
||||
return;
|
||||
}
|
||||
const diff = (text1, text2) => {
|
||||
const words1 = text1.split(' ');
|
||||
const words2 = text2.split(' ');
|
||||
|
||||
// Create a 2D array to hold our "matrix"
|
||||
const matrix = Array(words1.length + 1).fill(null).map(() => Array(words2.length + 1).fill(0));
|
||||
|
||||
// Perform standard LCS algorithm
|
||||
for (let i = 1; i <= words1.length; i++) {
|
||||
for (let j = 1; j <= words2.length; j++) {
|
||||
if (words1[i - 1] === words2[j - 1]) {
|
||||
matrix[i][j] = matrix[i - 1][j - 1] + 1;
|
||||
} else {
|
||||
matrix[i][j] = Math.max(matrix[i][j - 1], matrix[i - 1][j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let i = words1.length;
|
||||
let j = words2.length;
|
||||
const differences = [];
|
||||
|
||||
// Backtrack through the matrix to create the diff
|
||||
while (i > 0 || j > 0) {
|
||||
if (i > 0 && j > 0 && words1[i - 1] === words2[j - 1]) {
|
||||
differences.unshift(['black', words1[i - 1]]);
|
||||
i--;
|
||||
j--;
|
||||
} else if (j > 0 && (i === 0 || matrix[i][j - 1] >= matrix[i - 1][j])) {
|
||||
differences.unshift(['green', words2[j - 1]]);
|
||||
j--;
|
||||
} else if (i > 0 && (j === 0 || matrix[i][j - 1] < matrix[i - 1][j])) {
|
||||
differences.unshift(['red', words1[i - 1]]);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
return differences;
|
||||
};
|
||||
|
||||
|
||||
const differences = diff(text1, text2);
|
||||
|
||||
const displayDifferences = (differences) => {
|
||||
const resultDiv1 = document.getElementById("result1");
|
||||
const resultDiv2 = document.getElementById("result2");
|
||||
resultDiv1.innerHTML = "";
|
||||
resultDiv2.innerHTML = "";
|
||||
|
||||
differences.forEach(([color, word]) => {
|
||||
const span1 = document.createElement("span");
|
||||
const span2 = document.createElement("span");
|
||||
|
||||
// If it's an addition, show it in green in the second document and transparent in the first
|
||||
if (color === "green") {
|
||||
span1.style.color = "transparent";
|
||||
span1.style.userSelect = "none";
|
||||
span2.style.color = color;
|
||||
}
|
||||
// If it's a deletion, show it in red in the first document and transparent in the second
|
||||
else if (color === "red") {
|
||||
span1.style.color = color;
|
||||
span2.style.color = "transparent";
|
||||
span2.style.userSelect = "none";
|
||||
}
|
||||
// If it's unchanged, show it in black in both
|
||||
else {
|
||||
span1.style.color = color;
|
||||
span2.style.color = color;
|
||||
}
|
||||
|
||||
span1.textContent = word;
|
||||
span2.textContent = word;
|
||||
resultDiv1.appendChild(span1);
|
||||
resultDiv2.appendChild(span2);
|
||||
|
||||
// Add space after each word, or a new line if the word ends with a full stop
|
||||
const spaceOrNewline1 = document.createElement("span");
|
||||
const spaceOrNewline2 = document.createElement("span");
|
||||
if (word.endsWith(".")) {
|
||||
spaceOrNewline1.innerHTML = "<br>";
|
||||
spaceOrNewline2.innerHTML = "<br>";
|
||||
} else {
|
||||
spaceOrNewline1.textContent = " ";
|
||||
spaceOrNewline2.textContent = " ";
|
||||
}
|
||||
resultDiv1.appendChild(spaceOrNewline1);
|
||||
resultDiv2.appendChild(spaceOrNewline2);
|
||||
});
|
||||
};
|
||||
|
||||
console.log('Differences:', differences);
|
||||
displayDifferences(differences);
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
||||
</div>
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
|
|
57
src/main/resources/templates/other/flatten.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
|
||||
|
||||
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{flatten.title})}"></th:block>
|
||||
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
<br> <br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h2 th:text="#{flatten.header}"></h2>
|
||||
<form id="pdfForm" class="mb-3">
|
||||
<div class="custom-file">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf', remoteCall='false')}"></div>
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{flatten.submit}"></button>
|
||||
<script src="js/local-pdf-input-download.js"></script>
|
||||
<script>
|
||||
document.getElementById('pdfForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { PDFDocument } = PDFLib;
|
||||
|
||||
const processFile = async (file) => {
|
||||
const origFileUrl = URL.createObjectURL(file);
|
||||
const formPdfBytes = await fetch(origFileUrl).then(res => res.arrayBuffer());
|
||||
const pdfDoc = await PDFDocument.load(formPdfBytes);
|
||||
|
||||
const form = pdfDoc.getForm();
|
||||
form.flatten();
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
const pdfBlob = new Blob([pdfBytes], { type: 'application/pdf' });
|
||||
const fileName = (file.name ? file.name.replace('.pdf', '') : 'pdf') + '_flattened.pdf';
|
||||
|
||||
return { processedData: pdfBlob, fileName };
|
||||
};
|
||||
|
||||
await downloadFilesWithCallback(processFile);
|
||||
});
|
||||
</script>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
|
|
39
src/main/resources/templates/other/remove-blanks.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
|
||||
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{removeBlanks.title})}"></th:block>
|
||||
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
<br> <br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h2 th:text="#{removeBlanks.header}"></h2>
|
||||
|
||||
<form id="multiPdfForm" th:action="@{remove-blanks}" method="post" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
|
||||
<div class="form-group">
|
||||
<label for="threshold" th:text="#{removeBlanks.threshold}"></label>
|
||||
<input type="number" class="form-control" id="threshold" name="threshold" value="10">
|
||||
<small id="thresholdHelp" class="form-text text-muted" th:text="#{removeBlanks.thresholdDesc}"></small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="whitePercent" th:text="#{removeBlanks.whitePercent}"></label>
|
||||
<input type="number" class="form-control" id="whitePercent" name="whitePercent" value="99.9" step="0.1">
|
||||
<small id="whitePercentHelp" class="form-text text-muted" th:text="#{removeBlanks.whitePercentDesc}"></small>
|
||||
</div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{removeBlanks.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
29
src/main/resources/templates/other/repair.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
|
||||
|
||||
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{repair.title})}"></th:block>
|
||||
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
<br> <br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h2 th:text="#{repair.header}"></h2>
|
||||
<form id="multiPdfForm" th:action="@{repair}" method="post" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{repair.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
291
src/main/resources/templates/sign.html
Normal file
|
@ -0,0 +1,291 @@
|
|||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{sign.title})}"></th:block>
|
||||
<script src="js/signature_pad.umd.min.js"></script>
|
||||
<script src="js/interact.min.js"></script>
|
||||
|
||||
</head>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Estonia';
|
||||
src: url(fonts/Estonia.woff2) format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Tangerine';
|
||||
src: url(fonts/Tangerine.woff2) format('woff2');
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
<br> <br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h2 th:text="#{sign.header}"></h2>
|
||||
|
||||
<!-- pdf selector -->
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multiple=false, accept='application/pdf')}"></div>
|
||||
<script>
|
||||
let originalFileName = '';
|
||||
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
originalFileName = file.name.replace(/\.[^/.]+$/, "");
|
||||
const pdfData = await file.arrayBuffer();
|
||||
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
await DraggableUtils.renderPage(pdfDoc, 0);
|
||||
|
||||
document.querySelectorAll(".show-on-file-selected").forEach(el => {
|
||||
el.style.cssText = '';
|
||||
})
|
||||
}
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.querySelectorAll(".show-on-file-selected").forEach(el => {
|
||||
el.style.cssText = "display:none !important";
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="tab-group show-on-file-selected">
|
||||
<div class="tab-container" th:title="#{sign.upload}">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='image-upload', multiple=true, accept='image/*', inputText=#{imgPrompt})}"></div>
|
||||
<script>
|
||||
const imageUpload = document.querySelector('input[name=image-upload]');
|
||||
imageUpload.addEventListener('change', e => {
|
||||
if(!e.target.files) {
|
||||
return;
|
||||
}
|
||||
for (const imageFile of e.target.files) {
|
||||
var reader = new FileReader();
|
||||
reader.readAsDataURL(imageFile);
|
||||
reader.onloadend = function (e) {
|
||||
DraggableUtils.createDraggableCanvasFromUrl(e.target.result);
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
<div class="tab-container drawing-pad-container" th:title="#{sign.draw}">
|
||||
<canvas id="drawing-pad-canvas"></canvas>
|
||||
<br>
|
||||
<button id="clear-signature" class="btn btn-outline-danger mt-2" onclick="signaturePad.clear()" th:text="#{sign.clear}"></button>
|
||||
<button id="save-signature" class="btn btn-outline-success mt-2" onclick="addDraggableFromPad()" th:text="#{sign.add}"></button>
|
||||
<script>
|
||||
const signaturePadCanvas = document.getElementById('drawing-pad-canvas');
|
||||
const signaturePad = new SignaturePad(signaturePadCanvas, {
|
||||
minWidth: 1,
|
||||
maxWidth: 2,
|
||||
penColor: 'black',
|
||||
});
|
||||
function addDraggableFromPad() {
|
||||
if (signaturePad.isEmpty()) return;
|
||||
const startTime = Date.now();
|
||||
const croppedDataUrl = getCroppedCanvasDataUrl(signaturePadCanvas)
|
||||
console.log(Date.now() - startTime);
|
||||
DraggableUtils.createDraggableCanvasFromUrl(croppedDataUrl);
|
||||
}
|
||||
function getCroppedCanvasDataUrl(canvas) {
|
||||
// code is from: https://github.com/szimek/signature_pad/issues/49#issuecomment-1104035775
|
||||
let originalCtx = canvas.getContext('2d');
|
||||
|
||||
let originalWidth = canvas.width;
|
||||
let originalHeight = canvas.height;
|
||||
let imageData = originalCtx.getImageData(0,0, originalWidth, originalHeight);
|
||||
|
||||
let minX = originalWidth + 1, maxX = -1, minY = originalHeight + 1, maxY = -1, x = 0, y = 0, currentPixelColorValueIndex;
|
||||
|
||||
for (y = 0; y < originalHeight; y++) {
|
||||
for (x = 0; x < originalWidth; x++) {
|
||||
currentPixelColorValueIndex = (y * originalWidth + x) * 4;
|
||||
let currentPixelAlphaValue = imageData.data[currentPixelColorValueIndex + 3];
|
||||
if (currentPixelAlphaValue > 0) {
|
||||
if (minX > x) minX = x;
|
||||
if (maxX < x) maxX = x;
|
||||
if (minY > y) minY = y;
|
||||
if (maxY < y) maxY = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let croppedWidth = maxX - minX;
|
||||
let croppedHeight = maxY - minY;
|
||||
if (croppedWidth < 0 || croppedHeight < 0) return null;
|
||||
let cuttedImageData = originalCtx.getImageData(minX, minY, croppedWidth, croppedHeight);
|
||||
|
||||
let croppedCanvas = document.createElement('canvas'),
|
||||
croppedCtx = croppedCanvas.getContext('2d');
|
||||
|
||||
croppedCanvas.width = croppedWidth;
|
||||
croppedCanvas.height = croppedHeight;
|
||||
croppedCtx.putImageData(cuttedImageData, 0, 0);
|
||||
|
||||
return croppedCanvas.toDataURL();
|
||||
}
|
||||
function resizeCanvas() {
|
||||
// When zoomed out to less than 100%, for some very strange reason,
|
||||
// some browsers report devicePixelRatio as less than 1
|
||||
// and only part of the canvas is cleared then.
|
||||
var ratio = Math.max(window.devicePixelRatio || 1, 1);
|
||||
|
||||
// This part causes the canvas to be cleared
|
||||
signaturePadCanvas.width = signaturePadCanvas.offsetWidth * ratio;
|
||||
signaturePadCanvas.height = signaturePadCanvas.offsetHeight * ratio;
|
||||
signaturePadCanvas.getContext("2d").scale(ratio, ratio);
|
||||
|
||||
// This library does not listen for canvas changes, so after the canvas is automatically
|
||||
// cleared by the browser, SignaturePad#isEmpty might still return false, even though the
|
||||
// canvas looks empty, because the internal data of this library wasn't cleared. To make sure
|
||||
// that the state of this library is consistent with visual state of the canvas, you
|
||||
// have to clear it manually.
|
||||
signaturePad.clear();
|
||||
}
|
||||
new IntersectionObserver((entries, observer) => {
|
||||
if (entries.some(entry => entry.intersectionRatio > 0)) {
|
||||
resizeCanvas();
|
||||
}
|
||||
}).observe(signaturePadCanvas);
|
||||
new ResizeObserver(resizeCanvas).observe(signaturePadCanvas);
|
||||
</script>
|
||||
<style>
|
||||
.drawing-pad-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#drawing-pad-canvas {
|
||||
background: rgba(125,125,125,0.2);
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
<div class="tab-container" th:title="#{sign.text}">
|
||||
<label class="form-check-label" for="sigText" th:text="#{text}"></label>
|
||||
<input type="text" class="form-control" id="sigText" name="sigText">
|
||||
<label th:text="#{font}"></label>
|
||||
<select class="form-control" name="font" id="font-select">
|
||||
<option value="Estonia" class="estonia-font">Estonia</option>
|
||||
<option value="Tangerine" class="tangerine-font">Tangerine</option>
|
||||
</select>
|
||||
<div class="margin-auto-parent">
|
||||
<button id="save-text-signature" class="btn btn-outline-success mt-2 margin-center" onclick="addDraggableFromText()" th:text="#{sign.add}"></button>
|
||||
</div>
|
||||
<script>
|
||||
function addDraggableFromText() {
|
||||
const sigText = document.getElementById('sigText').value;
|
||||
const font = document.querySelector('select[name=font]').value;
|
||||
const fontSize = 100;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.font = `${fontSize}px ${font}`;
|
||||
const textWidth = ctx.measureText(sigText).width;
|
||||
const textHeight = fontSize;
|
||||
|
||||
canvas.width = textWidth;
|
||||
canvas.height = textHeight*1.35; //for tails
|
||||
ctx.font = `${fontSize}px ${font}`;
|
||||
ctx.fillText(sigText, 0, fontSize);
|
||||
|
||||
const dataURL = canvas.toDataURL();
|
||||
DraggableUtils.createDraggableCanvasFromUrl(dataURL);
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#font-select option {
|
||||
font-size: 30px;
|
||||
}
|
||||
#font-select option[value="Estonia"] {
|
||||
font-family: 'Estonia', sans-serif;
|
||||
}
|
||||
#font-select option[value="Tangerine"] {
|
||||
font-family: 'Tangerine', cursive;
|
||||
}
|
||||
#font-select option[value="Windsong"] {
|
||||
font-family: 'Windsong', cursive;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- draggables box -->
|
||||
<div id="box-drag-container" class="show-on-file-selected">
|
||||
<canvas id="pdf-canvas"></canvas>
|
||||
<script src="js/draggable-utils.js"></script>
|
||||
<div class="draggable-buttons-box ignore-rtl">
|
||||
<button class="btn btn-outline-secondary" onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/>
|
||||
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('lang-direction')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()" style="margin-left:auto">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-left" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('lang-direction')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-right" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<style>
|
||||
#box-drag-container {
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
}
|
||||
#pdf-canvas {
|
||||
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.384);
|
||||
width: 100%;
|
||||
}
|
||||
.draggable-buttons-box {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
.draggable-buttons-box > button {
|
||||
z-index: 10;
|
||||
background-color: rgba(13, 110, 253, 0.1);
|
||||
}
|
||||
.draggable-canvas {
|
||||
border: 1px solid red;
|
||||
position: absolute;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
top: 0px;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
|
||||
<!-- download button -->
|
||||
<div class="margin-auto-parent">
|
||||
<button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center">Download PDF</button>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById("download-pdf").addEventListener('click', async() => {
|
||||
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
|
||||
const modifiedPdfBytes = await modifiedPdf.save();
|
||||
const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = originalFileName + '_signed.pdf';
|
||||
link.click();
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|