utf8 bug fix and scan pages (#113)

This commit is contained in:
Anthony Stirling 2023-05-01 21:57:48 +01:00 committed by GitHub
parent 2d4aff3b08
commit 5bee714437
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 2493 additions and 1341 deletions

View file

@ -1,5 +1,9 @@
# Build jbig2enc in a separate stage # Build jbig2enc in a separate stage
FROM frooodle/stirling-pdf-base:latest FROM frooodle/stirling-pdf-base:beta2
# Create scripts folder and copy local scripts
RUN mkdir /scripts
COPY ./scripts/* /scripts/
# Copy the application JAR file # Copy the application JAR file
COPY build/libs/*.jar app.jar COPY build/libs/*.jar app.jar
@ -13,7 +17,8 @@ ENV APP_HOME_NAME="Stirling PDF"
#ENV APP_NAVBAR_NAME="Stirling PDF" #ENV APP_NAVBAR_NAME="Stirling PDF"
# Run the application # Run the application
ENTRYPOINT java -jar /app.jar ENTRYPOINT ["/scripts/init.sh"]
CMD ["java", "-jar", "/app.jar"]

View file

@ -21,10 +21,9 @@ RUN git clone https://github.com/agl/jbig2enc && \
make && \ make && \
make install make install
# Main stage
FROM openjdk:17-jdk-slim
# Install necessary dependencies # Main stage
FROM openjdk:17-jdk-slim AS base
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
libreoffice-core \ libreoffice-core \
@ -33,12 +32,31 @@ RUN apt-get update && \
libreoffice-calc \ libreoffice-calc \
libreoffice-impress \ libreoffice-impress \
python3-uno \ python3-uno \
python3-pip \ python3-pip \
unoconv \ unoconv \
pngquant \ pngquant \
unpaper \ unpaper \
ocrmypdf && \ ocrmypdf && \
pip install --user --upgrade ocrmypdf rm -rf /var/lib/apt/lists/* && \
mkdir /usr/share/tesseract-ocr-original && \
cp -r /usr/share/tesseract-ocr/* /usr/share/tesseract-ocr-original && \
rm -rf /usr/share/tesseract-ocr
# Copy the jbig2enc binary from the builder stage # Python packages stage
FROM base AS python-packages
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
libffi-dev \
libssl-dev \
zlib1g-dev \
libjpeg-dev && \
pip install --upgrade pip && \
pip install --no-cache-dir \
opencv-python-headless && \
rm -rf /var/lib/apt/lists/*
# Final stage: Copy necessary files from the previous stage
FROM base
COPY --from=python-packages /usr/local /usr/local
COPY --from=jbig2enc_builder /usr/local/bin/jbig2 /usr/local/bin/jbig2 COPY --from=jbig2enc_builder /usr/local/bin/jbig2 /usr/local/bin/jbig2

View file

@ -20,6 +20,8 @@ Depending on your requirements, you can choose the appropriate language pack for
1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need. 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` 2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tesseract-ocr/4.00/tessdata`
# DO NOT REMOVE EXISTING ENG.TRAINEDDATA, ITS REQUIRED.
#### Docker #### Docker
If you are using Docker, you need to expose the Tesseract tessdata directory as a volume in order to use the additional language packs. If you are using Docker, you need to expose the Tesseract tessdata directory as a volume in order to use the additional language packs.

View file

@ -99,3 +99,7 @@ Simply use environment variables APP_HOME_NAME, APP_HOME_DESCRIPTION and APP_NAV
If running Java directly, you can also pass these as properties using -D arguments. 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)
## 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
[here](https://app.swaggerhub.com/apis-docs/Frooodle/Stirling-PDF/1.0.0) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation

View file

@ -5,7 +5,7 @@ plugins {
} }
group = 'stirling.software' group = 'stirling.software'
version = '0.6.0' version = '0.7.0'
sourceCompatibility = '17' sourceCompatibility = '17'
repositories { repositories {
@ -20,10 +20,10 @@ dependencies {
implementation group: 'org.apache.pdfbox', name: 'jbig2-imageio', version: '3.0.4' implementation group: 'org.apache.pdfbox', name: 'jbig2-imageio', version: '3.0.4'
implementation 'commons-io:commons-io:2.11.0' implementation 'commons-io:commons-io:2.11.0'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
//general PDF //general PDF
implementation 'org.apache.pdfbox:pdfbox:2.0.28' implementation 'org.apache.pdfbox:pdfbox:2.0.28'
implementation 'com.itextpdf:itextpdf:5.5.13.3'
developmentOnly("org.springframework.boot:spring-boot-devtools") developmentOnly("org.springframework.boot:spring-boot-devtools")
} }

9
scripts/init.sh Normal file
View file

@ -0,0 +1,9 @@
#!/bin/bash
# Copy the original tesseract-ocr files to the volume directory without overwriting existing files
echo "Copying original files without overwriting existing files"
mkdir -p /usr/share/tesseract-ocr
cp -rn /usr/share/tesseract-ocr-original/* /usr/share/tesseract-ocr
# Run the main command
exec "$@"

134
scripts/split_photos.py Normal file
View file

@ -0,0 +1,134 @@
import sys
import cv2
import numpy as np
import os
def find_photo_boundaries(image, background_color, tolerance=30, min_area=10000, min_contour_area=500):
mask = cv2.inRange(image, background_color - tolerance, background_color + tolerance)
mask = cv2.bitwise_not(mask)
kernel = np.ones((5,5),np.uint8)
mask = cv2.dilate(mask, kernel, iterations=2)
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
photo_boundaries = []
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
area = w * h
contour_area = cv2.contourArea(contour)
if area >= min_area and contour_area >= min_contour_area:
photo_boundaries.append((x, y, w, h))
return photo_boundaries
def estimate_background_color(image, sample_points=5):
h, w, _ = image.shape
points = [
(0, 0),
(w - 1, 0),
(w - 1, h - 1),
(0, h - 1),
(w // 2, h // 2),
]
colors = []
for x, y in points:
colors.append(image[y, x])
return np.median(colors, axis=0)
def auto_rotate(image, angle_threshold=10):
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
ret, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) == 0:
return image
largest_contour = max(contours, key=cv2.contourArea)
mu = cv2.moments(largest_contour)
if mu["m00"] == 0:
return image
x_centroid = int(mu["m10"] / mu["m00"])
y_centroid = int(mu["m01"] / mu["m00"])
coords = np.column_stack(np.where(binary > 0))
u, _, vt = np.linalg.svd(coords - np.array([[y_centroid, x_centroid]]), full_matrices=False)
angle = np.arctan2(u[1, 0], u[0, 0]) * 180 / np.pi
if angle < -45:
angle = -(90 + angle)
else:
angle = -angle
if abs(angle) < angle_threshold:
return image
(h, w) = image.shape[:2]
center = (w // 2, h // 2)
M = cv2.getRotationMatrix2D(center, angle, 1.0)
return cv2.warpAffine(image, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE)
def crop_borders(image, border_color, tolerance=30):
mask = cv2.inRange(image, border_color - tolerance, border_color + tolerance)
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) == 0:
return image
largest_contour = max(contours, key=cv2.contourArea)
x, y, w, h = cv2.boundingRect(largest_contour)
return image[y:y+h, x:x+w]
def split_photos(input_file, output_directory, tolerance=30, min_area=10000, min_contour_area=500, angle_threshold=10, border_size=0):
image = cv2.imread(input_file)
background_color = estimate_background_color(image)
# Add a constant border around the image
image = cv2.copyMakeBorder(image, border_size, border_size, border_size, border_size, cv2.BORDER_CONSTANT, value=background_color)
photo_boundaries = find_photo_boundaries(image, background_color, tolerance)
if not os.path.exists(output_directory):
os.makedirs(output_directory)
# Get the input file's base name without the extension
input_file_basename = os.path.splitext(os.path.basename(input_file))[0]
for idx, (x, y, w, h) in enumerate(photo_boundaries):
cropped_image = image[y:y+h, x:x+w]
cropped_image = auto_rotate(cropped_image, angle_threshold)
# Remove the added border
cropped_image = cropped_image[border_size:-border_size, border_size:-border_size]
output_path = os.path.join(output_directory, f"{input_file_basename}_{idx+1}.png")
cv2.imwrite(output_path, cropped_image)
print(f"Saved {output_path}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python3 split_photos.py <input_file> <output_directory> [tolerance] [min_area] [min_contour_area] [angle_threshold] [border_size]")
print("\nParameters:")
print(" <input_file> - The input scanned image containing multiple photos.")
print(" <output_directory> - The directory where the result images should be placed.")
print(" [tolerance] - Optional. Determines the range of color variation around the estimated background color (default: 30).")
print(" [min_area] - Optional. Sets the minimum area threshold for a photo (default: 10000).")
print(" [min_contour_area] - Optional. Sets the minimum contour area threshold for a photo (default: 500).")
print(" [angle_threshold] - Optional. Sets the minimum absolute angle required for the image to be rotated (default: 10).")
print(" [border_size] - Optional. Sets the size of the border added and removed to prevent white borders in the output (default: 0).")
sys.exit(1)
input_file = sys.argv[1]
output_directory = sys.argv[2]
tolerance = int(sys.argv[3]) if len(sys.argv) > 3 else 20
min_area = int(sys.argv[4]) if len(sys.argv) > 4 else 8000
min_contour_area = int(sys.argv[5]) if len(sys.argv) > 5 else 500
angle_threshold = int(sys.argv[6]) if len(sys.argv) > 6 else 60
border_size = int(sys.argv[7]) if len(sys.argv) > 7 else 0
split_photos(input_file, output_directory, tolerance=tolerance, min_area=min_area, min_contour_area=min_contour_area, angle_threshold=angle_threshold, border_size=border_size)

View file

@ -16,6 +16,7 @@ public class Beans implements WebMvcConfigurer {
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor()); registry.addInterceptor(localeChangeInterceptor());
registry.addInterceptor(new CleanUrlInterceptor());
} }
@Bean @Bean

View file

@ -0,0 +1,45 @@
package stirling.software.SPDF.config;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public class CleanUrlInterceptor implements HandlerInterceptor {
private static final Pattern LANG_PATTERN = Pattern.compile("&?lang=([^&]+)");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String queryString = request.getQueryString();
if (queryString != null && !queryString.isEmpty()) {
String requestURI = request.getRequestURI();
// Keep the lang parameter if it exists
Matcher langMatcher = LANG_PATTERN.matcher(queryString);
String langQueryString = langMatcher.find() ? "lang=" + langMatcher.group(1) : "";
// Check if there are any other query parameters besides the lang parameter
String remainingQueryString = queryString.replaceAll(LANG_PATTERN.pattern(), "").replaceAll("&+", "&").replaceAll("^&|&$", "");
if (!remainingQueryString.isEmpty()) {
// Redirect to the URL without other query parameters
String redirectUrl = requestURI + (langQueryString.isEmpty() ? "" : "?" + langQueryString);
response.sendRedirect(redirectUrl);
return false;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
}
}

View file

@ -0,0 +1,20 @@
package stirling.software.SPDF.config;
import org.springframework.context.annotation.Bean;
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() {
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")));
}
}

View file

@ -1,20 +0,0 @@
package stirling.software.SPDF.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class MultiToolController {
private static final Logger logger = LoggerFactory.getLogger(MultiToolController.class);
@GetMapping("/multi-tool")
public String multiToolForm(Model model) {
model.addAttribute("currentPage", "multi-tool");
return "multi-tool";
}
}

View file

@ -1,25 +0,0 @@
package stirling.software.SPDF.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class PdfController {
private static final Logger logger = LoggerFactory.getLogger(PdfController.class);
@GetMapping("/")
public String home(Model model) {
model.addAttribute("currentPage", "home");
return "home";
}
@GetMapping("/home")
public String root(Model model) {
return "redirect:/";
}
}

View file

@ -1,4 +1,4 @@
package stirling.software.SPDF.controller; package stirling.software.SPDF.controller.api;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -10,26 +10,18 @@ import org.apache.pdfbox.pdmodel.PDPageTree;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
@Controller @RestController
public class MergeController { public class MergeController {
private static final Logger logger = LoggerFactory.getLogger(MergeController.class); private static final Logger logger = LoggerFactory.getLogger(MergeController.class);
@GetMapping("/merge-pdfs")
public String hello(Model model) {
model.addAttribute("currentPage", "merge-pdfs");
return "merge-pdfs";
}
private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException { private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
// Create a new empty document // Create a new empty document
PDDocument mergedDoc = new PDDocument(); PDDocument mergedDoc = new PDDocument();
@ -42,14 +34,16 @@ public class MergeController {
for (PDPage page : pages) { for (PDPage page : pages) {
mergedDoc.addPage(page); mergedDoc.addPage(page);
} }
} }
// Return the merged document // Return the merged document
return mergedDoc; return mergedDoc;
} }
@PostMapping("/merge-pdfs") @PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
public ResponseEntity<byte[]> mergePdfs(@RequestParam("fileInput") MultipartFile[] files) throws IOException { public ResponseEntity<byte[]> mergePdfs(@RequestPart(required = true, value = "fileInput") MultipartFile[] files) throws IOException {
// Read the input PDF files into PDDocument objects // Read the input PDF files into PDDocument objects
List<PDDocument> documents = new ArrayList<>(); List<PDDocument> documents = new ArrayList<>();
@ -60,8 +54,16 @@ public class MergeController {
PDDocument mergedDoc = mergeDocuments(documents); PDDocument mergedDoc = mergeDocuments(documents);
// Return the merged PDF as a response // Return the merged PDF as a response
return PdfUtils.pdfDocToWebResponse(mergedDoc, files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf"); ResponseEntity<byte[]> response = PdfUtils.pdfDocToWebResponse(mergedDoc, files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf");
for (PDDocument doc : documents) {
// Close the document after processing
doc.close();
}
return response;
} }
} }

View file

@ -1,4 +1,4 @@
package stirling.software.SPDF.controller; package stirling.software.SPDF.controller.api;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -9,22 +9,22 @@ import org.apache.pdfbox.pdmodel.PDPage;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
@Controller @RestController
public class RearrangePagesPDFController { public class RearrangePagesPDFController {
private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class); private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class);
@PostMapping("/remove-pages") @PostMapping(consumes = "multipart/form-data", value = "/remove-pages")
public ResponseEntity<byte[]> deletePages(@RequestParam("fileInput") MultipartFile pdfFile, @RequestParam("pagesToDelete") String pagesToDelete) throws IOException { public ResponseEntity<byte[]> deletePages(@RequestPart(required = true, value = "fileInput") MultipartFile pdfFile, @RequestParam("pagesToDelete") String pagesToDelete)
throws IOException {
PDDocument document = PDDocument.load(pdfFile.getBytes()); PDDocument document = PDDocument.load(pdfFile.getBytes());
@ -41,12 +41,6 @@ public class RearrangePagesPDFController {
} }
@GetMapping("/remove-pages")
public String pageDeleter(Model model) {
model.addAttribute("currentPage", "remove-pages");
return "remove-pages";
}
private List<Integer> pageOrderToString(String[] pageOrderArr, int totalPages) { private List<Integer> pageOrderToString(String[] pageOrderArr, int totalPages) {
List<Integer> newPageOrder = new ArrayList<>(); List<Integer> newPageOrder = new ArrayList<>();
// loop through the page order array // loop through the page order array
@ -75,14 +69,8 @@ public class RearrangePagesPDFController {
return newPageOrder; return newPageOrder;
} }
@GetMapping("/pdf-organizer") @PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages")
public String pageOrganizer(Model model) { public ResponseEntity<byte[]> rearrangePages(@RequestPart(required = true, value = "fileInput") MultipartFile pdfFile, @RequestParam("pageOrder") String pageOrder) {
model.addAttribute("currentPage", "pdf-organizer");
return "pdf-organizer";
}
@PostMapping("/rearrange-pages")
public ResponseEntity<byte[]> rearrangePages(@RequestParam("fileInput") MultipartFile pdfFile, @RequestParam("pageOrder") String pageOrder) {
try { try {
// Load the input PDF // Load the input PDF
PDDocument document = PDDocument.load(pdfFile.getInputStream()); PDDocument document = PDDocument.load(pdfFile.getInputStream());

View file

@ -1,4 +1,4 @@
package stirling.software.SPDF.controller; package stirling.software.SPDF.controller.api;
import java.io.IOException; import java.io.IOException;
@ -8,22 +8,21 @@ import org.apache.pdfbox.pdmodel.PDPageTree;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
@Controller @RestController
public class RotationController { public class RotationController {
private static final Logger logger = LoggerFactory.getLogger(RotationController.class); private static final Logger logger = LoggerFactory.getLogger(RotationController.class);
@PostMapping("/rotate-pdf") @PostMapping(consumes = "multipart/form-data", value = "/rotate-pdf")
public ResponseEntity<byte[]> rotatePDF(@RequestParam("fileInput") MultipartFile pdfFile, @RequestParam("angle") Integer angle) throws IOException { public ResponseEntity<byte[]> rotatePDF(@RequestPart(required = true, value = "fileInput") MultipartFile pdfFile, @RequestParam("angle") Integer angle) throws IOException {
// Load the PDF document // Load the PDF document
PDDocument document = PDDocument.load(pdfFile.getBytes()); PDDocument document = PDDocument.load(pdfFile.getBytes());
@ -39,10 +38,4 @@ public class RotationController {
} }
@GetMapping("/rotate-pdf")
public String rotatePdfForm(Model model) {
model.addAttribute("currentPage", "rotate-pdf");
return "rotate-pdf";
}
} }

View file

@ -1,4 +1,4 @@
package stirling.software.SPDF.controller; package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
@ -21,20 +21,19 @@ import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@Controller @RestController
public class SplitPDFController { public class SplitPDFController {
private static final Logger logger = LoggerFactory.getLogger(SplitPDFController.class); private static final Logger logger = LoggerFactory.getLogger(SplitPDFController.class);
@PostMapping("/split-pages") @PostMapping(consumes = "multipart/form-data", value = "/split-pages")
public ResponseEntity<Resource> splitPdf(@RequestParam("fileInput") MultipartFile file, @RequestParam("pages") String pages) throws IOException { public ResponseEntity<Resource> splitPdf(@RequestPart(required = true, value = "fileInput") MultipartFile file, @RequestParam("pages") String pages) throws IOException {
// parse user input // parse user input
// open the pdf document // open the pdf document
@ -127,9 +126,4 @@ public class SplitPDFController {
.contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(resource.contentLength()).body(resource); .contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(resource.contentLength()).body(resource);
} }
@GetMapping("/split-pdfs")
public String splitPdfForm(Model model) {
model.addAttribute("currentPage", "split-pdfs");
return "split-pdfs";
}
} }

View file

@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.converters; package stirling.software.SPDF.controller.api.converters;
import java.io.IOException; import java.io.IOException;
@ -11,22 +11,21 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
@Controller @RestController
public class ConvertImgPDFController { public class ConvertImgPDFController {
private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class); private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class);
@PostMapping("/pdf-to-img") @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-img")
public ResponseEntity<Resource> convertToImage(@RequestParam("fileInput") MultipartFile file, @RequestParam("imageFormat") String imageFormat, 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 { @RequestParam("singleOrMultiple") String singleOrMultiple, @RequestParam("colorType") String colorType, @RequestParam("dpi") String dpi) throws IOException {
byte[] pdfBytes = file.getBytes(); byte[] pdfBytes = file.getBytes();
@ -62,21 +61,16 @@ public class ConvertImgPDFController {
} }
} }
@PostMapping("/img-to-pdf") @PostMapping(consumes = "multipart/form-data", value = "/img-to-pdf")
public ResponseEntity<byte[]> convertToPdf(@RequestParam("fileInput") MultipartFile[] file, @RequestParam(defaultValue = "false", name = "stretchToFit") boolean stretchToFit, public ResponseEntity<byte[]> convertToPdf(@RequestPart(required = true, value = "fileInput") MultipartFile[] file,
@RequestParam(defaultValue = "true", name = "autoRotate") boolean autoRotate) throws IOException { @RequestParam(defaultValue = "false", name = "stretchToFit") boolean stretchToFit, @RequestParam(defaultValue = "true", name = "autoRotate") boolean autoRotate)
throws IOException {
// Convert the file to PDF and get the resulting bytes // Convert the file to PDF and get the resulting bytes
System.out.println(stretchToFit); System.out.println(stretchToFit);
byte[] bytes = PdfUtils.imageToPdf(file, stretchToFit, autoRotate); byte[] bytes = PdfUtils.imageToPdf(file, stretchToFit, autoRotate);
return PdfUtils.bytesToWebResponse(bytes, file[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_coverted.pdf"); return PdfUtils.bytesToWebResponse(bytes, file[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_coverted.pdf");
} }
@GetMapping("/img-to-pdf")
public String convertToPdfForm(Model model) {
model.addAttribute("currentPage", "img-to-pdf");
return "convert/img-to-pdf";
}
private String getMediaType(String imageFormat) { private String getMediaType(String imageFormat) {
if (imageFormat.equalsIgnoreCase("PNG")) if (imageFormat.equalsIgnoreCase("PNG"))
return "image/png"; return "image/png";
@ -88,10 +82,5 @@ public class ConvertImgPDFController {
return "application/octet-stream"; return "application/octet-stream";
} }
@GetMapping("/pdf-to-img")
public String pdfToimgForm(Model model) {
model.addAttribute("currentPage", "pdf-to-img");
return "convert/pdf-to-img";
}
} }

View file

@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.converters; package stirling.software.SPDF.controller.api.converters;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@ -10,17 +10,15 @@ import java.util.List;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
@Controller @RestController
public class ConvertOfficeController { public class ConvertOfficeController {
public byte[] convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException { public byte[] convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
@ -50,20 +48,13 @@ public class ConvertOfficeController {
return pdfBytes; return pdfBytes;
} }
@GetMapping("/file-to-pdf")
public String convertToPdfForm(Model model) {
model.addAttribute("currentPage", "file-to-pdf");
return "convert/file-to-pdf";
}
private boolean isValidFileExtension(String fileExtension) { private boolean isValidFileExtension(String fileExtension) {
String extensionPattern = "^(?i)[a-z0-9]{2,4}$"; String extensionPattern = "^(?i)[a-z0-9]{2,4}$";
return fileExtension.matches(extensionPattern); return fileExtension.matches(extensionPattern);
} }
@PostMapping("/file-to-pdf") @PostMapping(consumes = "multipart/form-data", value = "/file-to-pdf")
public ResponseEntity<byte[]> processPdfWithOCR(@RequestParam("fileInput") MultipartFile inputFile) throws IOException, InterruptedException { public ResponseEntity<byte[]> processPdfWithOCR(@RequestPart(required = true, value = "fileInput") MultipartFile inputFile) throws IOException, InterruptedException {
// unused but can start server instance if startup time is to long // unused but can start server instance if startup time is to long
// LibreOfficeListener.getInstance().start(); // LibreOfficeListener.getInstance().start();

View file

@ -0,0 +1,52 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.IOException;
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 stirling.software.SPDF.utils.PDFToFile;
@RestController
public class ConvertPDFToOffice {
@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")
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")
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")
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")
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");
}
}

View file

@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.converters; package stirling.software.SPDF.controller.api.converters;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@ -6,23 +6,20 @@ import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
@Controller @RestController
public class ConvertPDFToPDFA { public class ConvertPDFToPDFA {
@PostMapping("/pdf-to-pdfa") @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-pdfa")
public ResponseEntity<byte[]> pdfToPdfA(@RequestParam("fileInput") MultipartFile inputFile) throws IOException, InterruptedException { public ResponseEntity<byte[]> pdfToPdfA(@RequestPart(required = true, value = "fileInput") MultipartFile inputFile) throws IOException, InterruptedException {
// Save the uploaded file to a temporary location // Save the uploaded file to a temporary location
Path tempInputFile = Files.createTempFile("input_", ".pdf"); Path tempInputFile = Files.createTempFile("input_", ".pdf");
@ -52,16 +49,7 @@ public class ConvertPDFToPDFA {
// Return the optimized PDF as a response // Return the optimized PDF as a response
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_PDFA.pdf"; String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_PDFA.pdf";
HttpHeaders headers = new HttpHeaders(); return PdfUtils.bytesToWebResponse(pdfBytes, outputFilename);
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("attachment", outputFilename);
return ResponseEntity.ok().headers(headers).body(pdfBytes);
}
@GetMapping("/pdf-to-pdfa")
public String pdfToPdfAForm(Model model) {
model.addAttribute("currentPage", "pdf-to-pdfa");
return "convert/pdf-to-pdfa";
} }
} }

View file

@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.other; package stirling.software.SPDF.controller.api.other;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@ -8,31 +8,23 @@ import java.util.List;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
@Controller @RestController
public class CompressController { public class CompressController {
private static final Logger logger = LoggerFactory.getLogger(CompressController.class); private static final Logger logger = LoggerFactory.getLogger(CompressController.class);
@GetMapping("/compress-pdf") @PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
public String compressPdfForm(Model model) { public ResponseEntity<byte[]> optimizePdf(@RequestPart(required = true, value = "fileInput") MultipartFile inputFile, @RequestParam("optimizeLevel") int optimizeLevel,
model.addAttribute("currentPage", "compress-pdf");
return "other/compress-pdf";
}
@PostMapping("/compress-pdf")
public ResponseEntity<byte[]> optimizePdf(@RequestParam("fileInput") MultipartFile inputFile, @RequestParam("optimizeLevel") int optimizeLevel,
@RequestParam(name = "fastWebView", required = false) Boolean fastWebView, @RequestParam(name = "jbig2Lossy", required = false) Boolean jbig2Lossy) @RequestParam(name = "fastWebView", required = false) Boolean fastWebView, @RequestParam(name = "jbig2Lossy", required = false) Boolean jbig2Lossy)
throws IOException, InterruptedException { throws IOException, InterruptedException {
@ -78,10 +70,7 @@ public class CompressController {
// Return the optimized PDF as a response // Return the optimized PDF as a response
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_Optimized.pdf"; String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_Optimized.pdf";
HttpHeaders headers = new HttpHeaders(); return PdfUtils.bytesToWebResponse(pdfBytes, outputFilename);
headers.setContentType(MediaType.APPLICATION_PDF);
headers.setContentDispositionFormData("attachment", outputFilename);
return ResponseEntity.ok().headers(headers).body(pdfBytes);
} }
} }

View file

@ -0,0 +1,130 @@
package stirling.software.SPDF.controller.api.other;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.imageio.ImageIO;
import org.apache.commons.io.FileUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
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 stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
@RestController
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 {
String fileName = inputFile.getOriginalFilename();
String extension = fileName.substring(fileName.lastIndexOf(".") + 1);
List<String> images = new ArrayList<>();
// Check if input file is a PDF
if (extension.equalsIgnoreCase("pdf")) {
// Load PDF document
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(inputFile.getBytes()))) {
PDFRenderer pdfRenderer = new PDFRenderer(document);
int pageCount = document.getNumberOfPages();
images = new ArrayList<>();
// Create images of all pages
for (int i = 0; i < pageCount; i++) {
// Create temp file to save the image
Path tempFile = Files.createTempFile("image_", ".png");
// Render image and save as temp file
BufferedImage image = pdfRenderer.renderImageWithDPI(i, 300);
ImageIO.write(image, "png", tempFile.toFile());
// Add temp file path to images list
images.add(tempFile.toString());
}
}
} else {
Path tempInputFile = Files.createTempFile("input_", "." + extension);
Files.copy(inputFile.getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING);
// Add input file path to images list
images.add(tempInputFile.toString());
}
List<byte[]> processedImageBytes = new ArrayList<>();
// Process each image
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)));
// Run CLI command
int returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV).runCommandWithOutputHandling(command);
// Read the output photos in temp directory
List<Path> tempOutputFiles = Files.list(tempDir).sorted().collect(Collectors.toList());
for (Path tempOutputFile : tempOutputFiles) {
byte[] imageBytes = Files.readAllBytes(tempOutputFile);
processedImageBytes.add(imageBytes);
}
// Clean up the temporary directory
FileUtils.deleteDirectory(tempDir.toFile());
}
// Create zip file if multiple images
if (processedImageBytes.size() > 1) {
String outputZipFilename = fileName.replaceFirst("[.][^.]+$", "") + "_processed.zip";
Path tempZipFile = Files.createTempFile("output_", ".zip");
try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) {
// Add processed images to the zip
for (int i = 0; i < processedImageBytes.size(); i++) {
ZipEntry entry = new ZipEntry(fileName.replaceFirst("[.][^.]+$", "") + "_" + (i + 1) + ".png");
zipOut.putNextEntry(entry);
zipOut.write(processedImageBytes.get(i));
zipOut.closeEntry();
}
}
byte[] zipBytes = Files.readAllBytes(tempZipFile);
// Clean up the temporary zip file
Files.delete(tempZipFile);
return PdfUtils.bytesToWebResponse(zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
} else {
// Return the processed image as a response
byte[] imageBytes = processedImageBytes.get(0);
return PdfUtils.bytesToWebResponse(imageBytes, fileName.replaceFirst("[.][^.]+$", "") + ".png", MediaType.IMAGE_PNG);
}
}
}

View file

@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.other; package stirling.software.SPDF.controller.api.other;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.Image; import java.awt.Image;
@ -18,26 +18,23 @@ import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@Controller import stirling.software.SPDF.utils.PdfUtils;
@RestController
public class ExtractImagesController { public class ExtractImagesController {
private static final Logger logger = LoggerFactory.getLogger(ExtractImagesController.class); private static final Logger logger = LoggerFactory.getLogger(ExtractImagesController.class);
@PostMapping("/extract-images") @PostMapping(consumes = "multipart/form-data", value = "/extract-images")
public ResponseEntity<Resource> extractImages(@RequestParam("fileInput") MultipartFile file, @RequestParam("format") String format) throws IOException { public ResponseEntity<byte[]> extractImages(@RequestPart(required = true, value = "fileInput") MultipartFile file, @RequestParam("format") String format) throws IOException {
System.out.println(System.currentTimeMillis() + "file=" + file.getName() + ", format=" + format); System.out.println(System.currentTimeMillis() + "file=" + file.getName() + ", format=" + format);
PDDocument document = PDDocument.load(file.getBytes()); PDDocument document = PDDocument.load(file.getBytes());
@ -98,24 +95,8 @@ public class ExtractImagesController {
// Create ByteArrayResource from byte array // Create ByteArrayResource from byte array
byte[] zipContents = baos.toByteArray(); byte[] zipContents = baos.toByteArray();
ByteArrayResource resource = new ByteArrayResource(zipContents);
// Set content disposition header to indicate that the response should be return PdfUtils.boasToWebResponse(baos, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM);
// downloaded as a file
HttpHeaders headers = new HttpHeaders();
headers.setContentLength(zipContents.length);
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_extracted-images.zip");
// Return ResponseEntity with ByteArrayResource and headers
return ResponseEntity.status(HttpStatus.OK).headers(headers)
.header("Cache-Control", "no-cache").contentType(MediaType.APPLICATION_OCTET_STREAM).body(resource);
}
@GetMapping("/extract-images")
public String extractImagesForm(Model model) {
model.addAttribute("currentPage", "extract-images");
return "other/extract-images";
} }
} }

View file

@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.other; package stirling.software.SPDF.controller.api.other;
import java.io.IOException; import java.io.IOException;
import java.text.ParseException; import java.text.ParseException;
@ -11,23 +11,17 @@ import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation; import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
@Controller @RestController
public class MetadataController { public class MetadataController {
@GetMapping("/change-metadata")
public String addWatermarkForm(Model model) {
model.addAttribute("currentPage", "change-metadata");
return "other/change-metadata";
}
private String checkUndefined(String entry) { private String checkUndefined(String entry) {
// Check if the string is "undefined" // Check if the string is "undefined"
@ -40,8 +34,8 @@ public class MetadataController {
} }
@PostMapping("/update-metadata") @PostMapping(consumes = "multipart/form-data", value = "/update-metadata")
public ResponseEntity<byte[]> metadata(@RequestParam("fileInput") MultipartFile pdfFile, 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 = "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 = "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 = "keywords", required = false) String keywords, @RequestParam(value = "modificationDate", required = false) String modificationDate,

View file

@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.other; package stirling.software.SPDF.controller.api.other;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -10,26 +10,24 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
@Controller @RestController
public class OCRController { public class OCRController {
private static final Logger logger = LoggerFactory.getLogger(OCRController.class); private static final Logger logger = LoggerFactory.getLogger(OCRController.class);
@ -44,28 +42,29 @@ public class OCRController {
.filter(lang -> !lang.equalsIgnoreCase("osd")).collect(Collectors.toList()); .filter(lang -> !lang.equalsIgnoreCase("osd")).collect(Collectors.toList());
} }
@GetMapping("/ocr-pdf") @PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
public ModelAndView ocrPdfPage() { public ResponseEntity<byte[]> processPdfWithOCR(@RequestPart(required = true, value = "fileInput") MultipartFile inputFile,
ModelAndView modelAndView = new ModelAndView("other/ocr-pdf"); @RequestParam("languages") List<String> selectedLanguages, @RequestParam(name = "sidecar", required = false) Boolean sidecar,
modelAndView.addObject("languages", getAvailableTesseractLanguages()); @RequestParam(name = "deskew", required = false) Boolean deskew, @RequestParam(name = "clean", required = false) Boolean clean,
modelAndView.addObject("currentPage", "ocr-pdf"); @RequestParam(name = "clean-final", required = false) Boolean cleanFinal, @RequestParam(name = "ocrType", required = false) String ocrType,
return modelAndView; @RequestParam(name = "ocrRenderType", required = false, defaultValue = "hocr") String ocrRenderType,
} @RequestParam(name = "removeImagesAfter", required = false) Boolean removeImagesAfter)
throws IOException, InterruptedException {
@PostMapping("/ocr-pdf")
public ResponseEntity<byte[]> processPdfWithOCR(@RequestParam("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) throws IOException, InterruptedException {
// --output-type pdfa // --output-type pdfa
if (selectedLanguages == null || selectedLanguages.size() < 1) { if (selectedLanguages == null || selectedLanguages.isEmpty()) {
throw new IOException("Please select at least one language."); throw new IOException("Please select at least one language.");
} }
// Validate and sanitize selected languages using regex if(!ocrRenderType.equals("hocr") && !ocrRenderType.equals("sandwich")) {
String languagePattern = "^[a-zA-Z]{3}$"; // Regex pattern for three-letter language codes throw new IOException("ocrRenderType wrong");
selectedLanguages = selectedLanguages.stream().filter(lang -> Pattern.matches(languagePattern, lang)).collect(Collectors.toList()); }
// Get available Tesseract languages
List<String> availableLanguages = getAvailableTesseractLanguages();
// Validate selected languages
selectedLanguages = selectedLanguages.stream().filter(availableLanguages::contains).toList();
if (selectedLanguages.isEmpty()) { if (selectedLanguages.isEmpty()) {
throw new IOException("None of the selected languages are valid."); throw new IOException("None of the selected languages are valid.");
@ -83,7 +82,8 @@ public class OCRController {
// Run OCR Command // Run OCR Command
String languageOption = String.join("+", selectedLanguages); String languageOption = String.join("+", selectedLanguages);
List<String> command = new ArrayList<>(Arrays.asList("ocrmypdf", "--verbose", "2", "--output-type", "pdf"));
List<String> command = new ArrayList<>(Arrays.asList("ocrmypdf", "--verbose", "2", "--output-type", "pdf", "--pdf-renderer" , ocrRenderType));
if (sidecar != null && sidecar) { if (sidecar != null && sidecar) {
sidecarTextPath = Files.createTempFile("sidecar", ".txt"); sidecarTextPath = Files.createTempFile("sidecar", ".txt");
@ -115,16 +115,27 @@ public class OCRController {
// Run CLI command // Run CLI command
int returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command); int returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command);
// Remove images from the OCR processed PDF if the flag is set to true
if (removeImagesAfter != null && removeImagesAfter) {
Path tempPdfWithoutImages = Files.createTempFile("output_", "_no_images.pdf");
List<String> gsCommand = Arrays.asList("gs", "-sDEVICE=pdfwrite", "-dFILTERIMAGE", "-o", tempPdfWithoutImages.toString(), tempOutputFile.toString());
int gsReturnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(gsCommand);
tempOutputFile = tempPdfWithoutImages;
}
// Read the OCR processed PDF file // Read the OCR processed PDF file
byte[] pdfBytes = Files.readAllBytes(tempOutputFile); byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
// Clean up the temporary files // Clean up the temporary files
Files.delete(tempInputFile); Files.delete(tempInputFile);
// Return the OCR processed PDF as a response // Return the OCR processed PDF as a response
String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.pdf"; String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.pdf";
HttpHeaders headers = new HttpHeaders();
if (sidecar != null && sidecar) { if (sidecar != null && sidecar) {
// Create a zip file containing both the PDF and the text file // Create a zip file containing both the PDF and the text file
String outputZipFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.zip"; String outputZipFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.zip";
@ -152,15 +163,11 @@ public class OCRController {
Files.delete(sidecarTextPath); Files.delete(sidecarTextPath);
// Return the zip file containing both the PDF and the text file // Return the zip file containing both the PDF and the text file
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); return PdfUtils.bytesToWebResponse(pdfBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDispositionFormData("attachment", outputZipFilename);
return ResponseEntity.ok().headers(headers).body(zipBytes);
} else { } else {
// Return the OCR processed PDF as a response // Return the OCR processed PDF as a response
Files.delete(tempOutputFile); Files.delete(tempOutputFile);
headers.setContentType(MediaType.APPLICATION_PDF); return PdfUtils.bytesToWebResponse(pdfBytes, outputFilename);
headers.setContentDispositionFormData("attachment", outputFilename);
return ResponseEntity.ok().headers(headers).body(pdfBytes);
} }
} }

View file

@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.other; package stirling.software.SPDF.controller.api.other;
import java.io.IOException; import java.io.IOException;
@ -6,29 +6,22 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
@Controller @RestController
public class OverlayImageController { public class OverlayImageController {
private static final Logger logger = LoggerFactory.getLogger(OverlayImageController.class); private static final Logger logger = LoggerFactory.getLogger(OverlayImageController.class);
@GetMapping("/add-image") @PostMapping(consumes = "multipart/form-data", value = "/add-image")
public String overlayImage(Model model) { public ResponseEntity<byte[]> overlayImage(@RequestPart(required = true, value = "fileInput") MultipartFile pdfFile, @RequestParam("fileInput2") MultipartFile imageFile,
model.addAttribute("currentPage", "add-image"); @RequestParam("x") float x, @RequestParam("y") float y, @RequestParam("everyPage") boolean everyPage) {
return "other/add-image";
}
@PostMapping("/add-image")
public ResponseEntity<byte[]> overlayImage(@RequestParam("fileInput") MultipartFile pdfFile, @RequestParam("fileInput2") MultipartFile imageFile, @RequestParam("x") float x,
@RequestParam("y") float y, @RequestParam("everyPage") boolean everyPage) {
try { try {
byte[] pdfBytes = pdfFile.getBytes(); byte[] pdfBytes = pdfFile.getBytes();
byte[] imageBytes = imageFile.getBytes(); byte[] imageBytes = imageFile.getBytes();

View file

@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.security; package stirling.software.SPDF.controller.api.security;
import java.io.IOException; import java.io.IOException;
@ -8,36 +8,32 @@ import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
@Controller @RestController
public class PasswordController { public class PasswordController {
private static final Logger logger = LoggerFactory.getLogger(PasswordController.class); private static final Logger logger = LoggerFactory.getLogger(PasswordController.class);
@GetMapping("/add-password")
public String addPasswordForm(Model model) {
model.addAttribute("currentPage", "add-password");
return "security/add-password";
}
@PostMapping("/remove-password") @PostMapping(consumes = "multipart/form-data", value = "/remove-password")
public ResponseEntity<byte[]> compressPDF(@RequestParam("fileInput") MultipartFile fileInput, @RequestParam(name = "password") String password) throws IOException { public ResponseEntity<byte[]> compressPDF(@RequestPart(required = true, value = "fileInput") MultipartFile fileInput, @RequestParam(name = "password") String password)
throws IOException {
PDDocument document = PDDocument.load(fileInput.getBytes(), password); PDDocument document = PDDocument.load(fileInput.getBytes(), password);
document.setAllSecurityToBeRemoved(true); document.setAllSecurityToBeRemoved(true);
return PdfUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_password_removed.pdf"); return PdfUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_password_removed.pdf");
} }
@PostMapping("/add-password") @PostMapping(consumes = "multipart/form-data", value = "/add-password")
public ResponseEntity<byte[]> compressPDF(@RequestParam("fileInput") MultipartFile fileInput, @RequestParam(defaultValue = "", name = "password") String password, public ResponseEntity<byte[]> compressPDF(@RequestPart(required = true, value = "fileInput") MultipartFile fileInput,
@RequestParam(defaultValue = "128", name = "keyLength") int keyLength, @RequestParam(defaultValue = "false", name = "canAssembleDocument") boolean canAssembleDocument, @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 = "canExtractContent") boolean canExtractContent,
@RequestParam(defaultValue = "false", name = "canExtractForAccessibility") boolean canExtractForAccessibility, @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 = "canFillInForm") boolean canFillInForm, @RequestParam(defaultValue = "false", name = "canModify") boolean canModify,
@ -66,16 +62,5 @@ public class PasswordController {
return PdfUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf"); return PdfUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf");
} }
@GetMapping("/change-permissions")
public String permissionsForm(Model model) {
model.addAttribute("currentPage", "change-permissions");
return "security/change-permissions";
}
@GetMapping("/remove-password")
public String removePasswordForm(Model model) {
model.addAttribute("currentPage", "remove-password");
return "security/remove-password";
}
} }

View file

@ -1,4 +1,4 @@
package stirling.software.SPDF.controller.security; package stirling.software.SPDF.controller.api.security;
import java.awt.Color; import java.awt.Color;
import java.io.IOException; import java.io.IOException;
@ -18,21 +18,20 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField; import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.util.Matrix; import org.apache.pdfbox.util.Matrix;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WatermarkRemover; import stirling.software.SPDF.utils.WatermarkRemover;
@Controller @RestController
public class WatermarkController { public class WatermarkController {
@PostMapping("/add-watermark") @PostMapping(consumes = "multipart/form-data", value = "/add-watermark")
public ResponseEntity<byte[]> addWatermark(@RequestParam("fileInput") MultipartFile pdfFile, @RequestParam("watermarkText") String watermarkText, 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 = "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 = "0.5", name = "opacity") float opacity, @RequestParam(defaultValue = "50", name = "widthSpacer") int widthSpacer,
@RequestParam(defaultValue = "50", name = "heightSpacer") int heightSpacer) throws IOException { @RequestParam(defaultValue = "50", name = "heightSpacer") int heightSpacer) throws IOException {
@ -81,14 +80,10 @@ public class WatermarkController {
return PdfUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf"); return PdfUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf");
} }
@GetMapping("/add-watermark")
public String addWatermarkForm(Model model) {
model.addAttribute("currentPage", "add-watermark");
return "security/add-watermark";
}
@PostMapping("/remove-watermark") @PostMapping(consumes = "multipart/form-data", value = "/remove-watermark")
public ResponseEntity<byte[]> removeWatermark(@RequestParam("fileInput") MultipartFile pdfFile, @RequestParam("watermarkText") String watermarkText) throws Exception { public ResponseEntity<byte[]> removeWatermark(@RequestPart(required = true, value = "fileInput") MultipartFile pdfFile, @RequestParam("watermarkText") String watermarkText)
throws Exception {
// Load the input PDF // Load the input PDF
PDDocument document = PDDocument.load(pdfFile.getInputStream()); PDDocument document = PDDocument.load(pdfFile.getInputStream());
@ -141,10 +136,5 @@ public class WatermarkController {
return PdfUtils.pdfDocToWebResponse(outputDocument, "removed.pdf"); return PdfUtils.pdfDocToWebResponse(outputDocument, "removed.pdf");
} }
@GetMapping("/remove-watermark")
public String removeWatermarkForm(Model model) {
model.addAttribute("currentPage", "remove-watermark");
return "security/remove-watermark";
}
} }

View file

@ -1,86 +0,0 @@
package stirling.software.SPDF.controller.converters;
import java.io.IOException;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;
import stirling.software.SPDF.utils.PDFToFile;
@Controller
public class ConvertPDFToOffice {
@GetMapping("/pdf-to-html")
public ModelAndView pdfToHTML() {
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-html");
modelAndView.addObject("currentPage", "pdf-to-html");
return modelAndView;
}
@GetMapping("/pdf-to-presentation")
public ModelAndView pdfToPresentation() {
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-presentation");
modelAndView.addObject("currentPage", "pdf-to-presentation");
return modelAndView;
}
@GetMapping("/pdf-to-text")
public ModelAndView pdfToText() {
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-text");
modelAndView.addObject("currentPage", "pdf-to-text");
return modelAndView;
}
@GetMapping("/pdf-to-word")
public ModelAndView pdfToWord() {
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-word");
modelAndView.addObject("currentPage", "pdf-to-word");
return modelAndView;
}
@GetMapping("/pdf-to-xml")
public ModelAndView pdfToXML() {
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-xml");
modelAndView.addObject("currentPage", "pdf-to-xml");
return modelAndView;
}
@PostMapping("/pdf-to-html")
public ResponseEntity<byte[]> processPdfToHTML(@RequestParam("fileInput") MultipartFile inputFile) throws IOException, InterruptedException {
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, "html", "writer_pdf_import");
}
@PostMapping("/pdf-to-presentation")
public ResponseEntity<byte[]> processPdfToPresentation(@RequestParam("fileInput") MultipartFile inputFile, @RequestParam("outputFormat") String outputFormat)
throws IOException, InterruptedException {
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import");
}
@PostMapping("/pdf-to-text")
public ResponseEntity<byte[]> processPdfToRTForTXT(@RequestParam("fileInput") MultipartFile inputFile, @RequestParam("outputFormat") String outputFormat)
throws IOException, InterruptedException {
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
}
@PostMapping("/pdf-to-word")
public ResponseEntity<byte[]> processPdfToWord(@RequestParam("fileInput") MultipartFile inputFile, @RequestParam("outputFormat") String outputFormat)
throws IOException, InterruptedException {
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
}
@PostMapping("/pdf-to-xml")
public ResponseEntity<byte[]> processPdfToXML(@RequestParam("fileInput") MultipartFile inputFile) throws IOException, InterruptedException {
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, "xml", "writer_pdf_import");
}
}

View file

@ -0,0 +1,86 @@
package stirling.software.SPDF.controller.web;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;
import io.swagger.v3.oas.annotations.Hidden;
@Controller
public class ConverterWebController {
@GetMapping("/img-to-pdf")
@Hidden
public String convertImgToPdfForm(Model model) {
model.addAttribute("currentPage", "img-to-pdf");
return "convert/img-to-pdf";
}
@GetMapping("/pdf-to-img")
@Hidden
public String pdfToimgForm(Model model) {
model.addAttribute("currentPage", "pdf-to-img");
return "convert/pdf-to-img";
}
@GetMapping("/file-to-pdf")
@Hidden
public String convertToPdfForm(Model model) {
model.addAttribute("currentPage", "file-to-pdf");
return "convert/file-to-pdf";
}
//PDF TO......
@GetMapping("/pdf-to-html")
@Hidden
public ModelAndView pdfToHTML() {
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-html");
modelAndView.addObject("currentPage", "pdf-to-html");
return modelAndView;
}
@GetMapping("/pdf-to-presentation")
@Hidden
public ModelAndView pdfToPresentation() {
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-presentation");
modelAndView.addObject("currentPage", "pdf-to-presentation");
return modelAndView;
}
@GetMapping("/pdf-to-text")
@Hidden
public ModelAndView pdfToText() {
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-text");
modelAndView.addObject("currentPage", "pdf-to-text");
return modelAndView;
}
@GetMapping("/pdf-to-word")
@Hidden
public ModelAndView pdfToWord() {
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-word");
modelAndView.addObject("currentPage", "pdf-to-word");
return modelAndView;
}
@GetMapping("/pdf-to-xml")
@Hidden
public ModelAndView pdfToXML() {
ModelAndView modelAndView = new ModelAndView("convert/pdf-to-xml");
modelAndView.addObject("currentPage", "pdf-to-xml");
return modelAndView;
}
@GetMapping("/pdf-to-pdfa")
@Hidden
public String pdfToPdfAForm(Model model) {
model.addAttribute("currentPage", "pdf-to-pdfa");
return "convert/pdf-to-pdfa";
}
}

View file

@ -0,0 +1,69 @@
package stirling.software.SPDF.controller.web;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import io.swagger.v3.oas.annotations.Hidden;
@Controller
public class GeneralWebController {
@GetMapping("/merge-pdfs")
@Hidden
public String mergePdfForm(Model model) {
model.addAttribute("currentPage", "merge-pdfs");
return "merge-pdfs";
}
@GetMapping("/about")
@Hidden
public String gameForm(Model model) {
model.addAttribute("currentPage", "about");
return "about";
}
@GetMapping("/multi-tool")
@Hidden
public String multiToolForm(Model model) {
model.addAttribute("currentPage", "multi-tool");
return "multi-tool";
}
@GetMapping("/")
public String home(Model model) {
model.addAttribute("currentPage", "home");
return "home";
}
@GetMapping("/home")
public String root(Model model) {
return "redirect:/";
}
@GetMapping("/remove-pages")
@Hidden
public String pageDeleter(Model model) {
model.addAttribute("currentPage", "remove-pages");
return "remove-pages";
}
@GetMapping("/pdf-organizer")
@Hidden
public String pageOrganizer(Model model) {
model.addAttribute("currentPage", "pdf-organizer");
return "pdf-organizer";
}
@GetMapping("/rotate-pdf")
@Hidden
public String rotatePdfForm(Model model) {
model.addAttribute("currentPage", "rotate-pdf");
return "rotate-pdf";
}
@GetMapping("/split-pdfs")
@Hidden
public String splitPdfForm(Model model) {
model.addAttribute("currentPage", "split-pdfs");
return "split-pdfs";
}
}

View file

@ -0,0 +1,84 @@
package stirling.software.SPDF.controller.web;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;
import io.swagger.v3.oas.annotations.Hidden;
@Controller
public class OtherWebController {
@GetMapping("/compress-pdf")
@Hidden
public String compressPdfForm(Model model) {
model.addAttribute("currentPage", "compress-pdf");
return "other/compress-pdf";
}
@GetMapping("/extract-image-scans")
@Hidden
public ModelAndView extractImageScansForm() {
ModelAndView modelAndView = new ModelAndView("other/extract-image-scans");
modelAndView.addObject("currentPage", "extract-image-scans");
return modelAndView;
}
@GetMapping("/extract-images")
@Hidden
public String extractImagesForm(Model model) {
model.addAttribute("currentPage", "extract-images");
return "other/extract-images";
}
@GetMapping("/change-metadata")
@Hidden
public String addWatermarkForm(Model model) {
model.addAttribute("currentPage", "change-metadata");
return "other/change-metadata";
}
public List<String> getAvailableTesseractLanguages() {
String tessdataDir = "/usr/share/tesseract-ocr/4.00/tessdata";
File[] files = new File(tessdataDir).listFiles();
if (files == null) {
return Collections.emptyList();
}
return Arrays.stream(files).filter(file -> file.getName().endsWith(".traineddata")).map(file -> file.getName().replace(".traineddata", ""))
.filter(lang -> !lang.equalsIgnoreCase("osd")).collect(Collectors.toList());
}
@GetMapping("/ocr-pdf")
@Hidden
public ModelAndView ocrPdfPage() {
ModelAndView modelAndView = new ModelAndView("other/ocr-pdf");
modelAndView.addObject("languages", getAvailableTesseractLanguages());
modelAndView.addObject("currentPage", "ocr-pdf");
return modelAndView;
}
@GetMapping("/add-image")
@Hidden
public String overlayImage(Model model) {
model.addAttribute("currentPage", "add-image");
return "other/add-image";
}
@GetMapping("/adjust-contrast")
@Hidden
public String contrast(Model model) {
model.addAttribute("currentPage", "adjust-contrast");
return "other/adjust-contrast";
}
}

View file

@ -0,0 +1,46 @@
package stirling.software.SPDF.controller.web;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import io.swagger.v3.oas.annotations.Hidden;
@Controller
public class SecurityWebController {
@GetMapping("/add-password")
@Hidden
public String addPasswordForm(Model model) {
model.addAttribute("currentPage", "add-password");
return "security/add-password";
}
@GetMapping("/change-permissions")
@Hidden
public String permissionsForm(Model model) {
model.addAttribute("currentPage", "change-permissions");
return "security/change-permissions";
}
@GetMapping("/remove-password")
@Hidden
public String removePasswordForm(Model model) {
model.addAttribute("currentPage", "remove-password");
return "security/remove-password";
}
@GetMapping("/add-watermark")
@Hidden
public String addWatermarkForm(Model model) {
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";
}
}

View file

@ -15,7 +15,6 @@ import java.util.zip.ZipOutputStream;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -41,8 +40,7 @@ public class PDFToFile {
Path tempInputFile = null; Path tempInputFile = null;
Path tempOutputDir = null; Path tempOutputDir = null;
byte[] fileBytes; byte[] fileBytes;
// Prepare response String fileName = "temp.file";
HttpHeaders headers = new HttpHeaders();
try { try {
// Save the uploaded file to a temporary location // Save the uploaded file to a temporary location
@ -63,16 +61,14 @@ public class PDFToFile {
if (outputFiles.size() == 1) { if (outputFiles.size() == 1) {
// Return single output file // Return single output file
File outputFile = outputFiles.get(0); File outputFile = outputFiles.get(0);
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
if (outputFormat.equals("txt:Text")) { if (outputFormat.equals("txt:Text")) {
outputFormat = "txt"; outputFormat = "txt";
} }
headers.setContentDispositionFormData("attachment", pdfBaseName + "." + outputFormat); fileName = pdfBaseName + "." + outputFormat;
fileBytes = FileUtils.readFileToByteArray(outputFile); fileBytes = FileUtils.readFileToByteArray(outputFile);
} else { } else {
// Return output files in a ZIP archive // Return output files in a ZIP archive
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); fileName = pdfBaseName + "To" + outputFormat + ".zip";
headers.setContentDispositionFormData("attachment", pdfBaseName + "To" + outputFormat + ".zip");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream); ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream);
@ -96,6 +92,6 @@ public class PDFToFile {
if (tempOutputDir != null) if (tempOutputDir != null)
FileUtils.deleteDirectory(tempOutputDir.toFile()); FileUtils.deleteDirectory(tempOutputDir.toFile());
} }
return new ResponseEntity<>(fileBytes, headers, HttpStatus.OK); return PdfUtils.bytesToWebResponse(fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
} }
} }

View file

@ -8,6 +8,8 @@ import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.KeyStore; import java.security.KeyStore;
@ -43,19 +45,27 @@ public class PdfUtils {
public static ResponseEntity<byte[]> boasToWebResponse(ByteArrayOutputStream baos, String docName) throws IOException { public static ResponseEntity<byte[]> boasToWebResponse(ByteArrayOutputStream baos, String docName) throws IOException {
return PdfUtils.bytesToWebResponse(baos.toByteArray(), docName); return PdfUtils.bytesToWebResponse(baos.toByteArray(), docName);
} }
public static ResponseEntity<byte[]> bytesToWebResponse(byte[] bytes, String docName) throws IOException { public static ResponseEntity<byte[]> boasToWebResponse(ByteArrayOutputStream baos, String docName, MediaType mediaType) throws IOException {
return PdfUtils.bytesToWebResponse(baos.toByteArray(), docName, mediaType);
}
public static ResponseEntity<byte[]> bytesToWebResponse(byte[] bytes, String docName, MediaType mediaType) throws IOException {
// Return the PDF as a response // Return the PDF as a response
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_PDF); headers.setContentType(mediaType);
headers.setContentLength(bytes.length); headers.setContentLength(bytes.length);
headers.setContentDispositionFormData("attachment", docName); String encodedDocName = URLEncoder.encode(docName, StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20");
headers.setContentDispositionFormData("attachment", encodedDocName);
return new ResponseEntity<>(bytes, headers, HttpStatus.OK); return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
} }
public static ResponseEntity<byte[]> bytesToWebResponse(byte[] bytes, String docName) throws IOException {
return bytesToWebResponse(bytes, docName, MediaType.APPLICATION_PDF);
}
public static byte[] convertFromPdf(byte[] inputStream, String imageType, ImageType colorType, boolean singleImage, int DPI) throws IOException, Exception { public static byte[] convertFromPdf(byte[] inputStream, String imageType, ImageType colorType, boolean singleImage, int DPI) throws IOException, Exception {
try (PDDocument document = PDDocument.load(new ByteArrayInputStream(inputStream))) { try (PDDocument document = PDDocument.load(new ByteArrayInputStream(inputStream))) {
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);

View file

@ -13,7 +13,7 @@ import java.util.concurrent.Semaphore;
public class ProcessExecutor { public class ProcessExecutor {
public enum Processes { public enum Processes {
LIBRE_OFFICE, OCR_MY_PDF LIBRE_OFFICE, OCR_MY_PDF, PYTHON_OPENCV, GHOSTSCRIPT
} }
private static final Map<Processes, ProcessExecutor> instances = new ConcurrentHashMap<>(); private static final Map<Processes, ProcessExecutor> instances = new ConcurrentHashMap<>();
@ -23,6 +23,8 @@ public class ProcessExecutor {
int semaphoreLimit = switch (key) { int semaphoreLimit = switch (key) {
case LIBRE_OFFICE -> 1; case LIBRE_OFFICE -> 1;
case OCR_MY_PDF -> 2; case OCR_MY_PDF -> 2;
case PYTHON_OPENCV -> 8;
case GHOSTSCRIPT -> 16;
}; };
return new ProcessExecutor(semaphoreLimit); return new ProcessExecutor(semaphoreLimit);
}); });

View file

@ -31,10 +31,10 @@ navbar.convert=تحويل
navbar.security=الأمان navbar.security=الأمان
navbar.other=أخرى navbar.other=أخرى
navbar.darkmode=الوضع الداكن navbar.darkmode=الوضع الداكن
navbar.pageOps = عمليات الصفحة navbar.pageOps=عمليات الصفحة
home.multiTool.title = أداة متعددة PDF home.multiTool.title=أداة متعددة PDF
home.multiTool.desc = دمج الصفحات وتدويرها وإعادة ترتيبها وإزالتها home.multiTool.desc=دمج الصفحات وتدويرها وإعادة ترتيبها وإزالتها
home.merge.title=دمج ملفات home.merge.title=دمج ملفات
home.merge.desc=دمج ملفات PDF متعددة في ملف واحد بسهولة. home.merge.desc=دمج ملفات PDF متعددة في ملف واحد بسهولة.
@ -91,24 +91,40 @@ home.ocr.desc=\u064A\u0642\u0648\u0645 \u0628\u0631\u0646\u0627\u0645\u062C \u06
home.extractImages.title=\u0627\u0633\u062A\u062E\u0631\u0627\u062C \u0627\u0644\u0635\u0648\u0631 home.extractImages.title=\u0627\u0633\u062A\u062E\u0631\u0627\u062C \u0627\u0644\u0635\u0648\u0631
home.extractImages.desc=\u064A\u0633\u062A\u062E\u0631\u062C \u062C\u0645\u064A\u0639 \u0627\u0644\u0635\u0648\u0631 \u0645\u0646 \u0645\u0644\u0641 PDF \u0648\u064A\u062D\u0641\u0638\u0647\u0627 \u0641\u064A \u0627\u0644\u0631\u0645\u0632 \u0627\u0644\u0628\u0631\u064A\u062F\u064A home.extractImages.desc=\u064A\u0633\u062A\u062E\u0631\u062C \u062C\u0645\u064A\u0639 \u0627\u0644\u0635\u0648\u0631 \u0645\u0646 \u0645\u0644\u0641 PDF \u0648\u064A\u062D\u0641\u0638\u0647\u0627 \u0641\u064A \u0627\u0644\u0631\u0645\u0632 \u0627\u0644\u0628\u0631\u064A\u062F\u064A
home.pdfToPDFA.title = \u062A\u062D\u0648\u064A\u0644 \u0645\u0644\u0641\u0627\u062A PDF \u0625\u0644\u0649 PDF / A home.pdfToPDFA.title=\u062A\u062D\u0648\u064A\u0644 \u0645\u0644\u0641\u0627\u062A PDF \u0625\u0644\u0649 PDF / A
home.pdfToPDFA.desc = \u062A\u062D\u0648\u064A\u0644 PDF \u0625\u0644\u0649 PDF / A \u0644\u0644\u062A\u062E\u0632\u064A\u0646 \u0637\u0648\u064A\u0644 \u0627\u0644\u0645\u062F\u0649 home.pdfToPDFA.desc=\u062A\u062D\u0648\u064A\u0644 PDF \u0625\u0644\u0649 PDF / A \u0644\u0644\u062A\u062E\u0632\u064A\u0646 \u0637\u0648\u064A\u0644 \u0627\u0644\u0645\u062F\u0649
home.PDFToWord.title = تحويل PDF إلى Word home.PDFToWord.title=تحويل PDF إلى Word
home.PDFToWord.desc = تحويل PDF إلى تنسيقات Word (DOC و DOCX و ODT) home.PDFToWord.desc=تحويل PDF إلى تنسيقات Word (DOC و DOCX و ODT)
home.PDFToPresentation.title = PDF للعرض التقديمي home.PDFToPresentation.title=PDF للعرض التقديمي
home.PDFToPresentation.desc = تحويل PDF إلى تنسيقات عرض تقديمي (PPT و PPTX و ODP) home.PDFToPresentation.desc=تحويل PDF إلى تنسيقات عرض تقديمي (PPT و PPTX و ODP)
home.PDFToText.title = تحويل PDF إلى نص / RTF home.PDFToText.title=تحويل PDF إلى نص / RTF
home.PDFToText.desc = تحويل PDF إلى تنسيق نص أو RTF home.PDFToText.desc=تحويل PDF إلى تنسيق نص أو RTF
home.PDFToHTML.title = تحويل PDF إلى HTML home.PDFToHTML.title=تحويل PDF إلى HTML
home.PDFToHTML.desc = تحويل PDF إلى تنسيق HTML home.PDFToHTML.desc=تحويل PDF إلى تنسيق HTML
home.PDFToXML.title=تحويل PDF إلى XML
home.PDFToXML.desc=تحويل PDF إلى تنسيق XML
home.ScannerImageSplit.title=كشف / انقسام الصور الممسوحة ضوئيًا
home.ScannerImageSplit.desc=تقسيم عدة صور من داخل صورة / ملف PDF
ScannerImageSplit.selectText.1=عتبة الزاوية:
ScannerImageSplit.selectText.2=تعيين الحد الأدنى للزاوية المطلقة المطلوبة لتدوير الصورة (افتراضي: 10).
ScannerImageSplit.selectText.3=التسامح:
ScannerImageSplit.selectText.4=يحدد نطاق تباين اللون حول لون الخلفية المقدر (الافتراضي: 30).
ScannerImageSplit.selectText.5=أدنى مساحة:
ScannerImageSplit.selectText.6=تعيين الحد الأدنى لمنطقة الصورة (الافتراضي: 10000).
ScannerImageSplit.selectText.7=الحد الأدنى لمنطقة المحيط:
ScannerImageSplit.selectText.8=تعيين الحد الأدنى لمنطقة المحيط للصورة
ScannerImageSplit.selectText.9=حجم الحدود:
ScannerImageSplit.selectText.10=يضبط حجم الحدود المضافة والمزالة لمنع الحدود البيضاء في الإخراج (الافتراضي: 1).
home.PDFToXML.title = تحويل PDF إلى XML
home.PDFToXML.desc = تحويل PDF إلى تنسيق XML
navbar.settings=\u0625\u0639\u062F\u0627\u062F\u0627\u062A navbar.settings=\u0625\u0639\u062F\u0627\u062F\u0627\u062A
settings.title=\u0627\u0644\u0625\u0639\u062F\u0627\u062F\u0627\u062A settings.title=\u0627\u0644\u0625\u0639\u062F\u0627\u062F\u0627\u062A
@ -133,6 +149,8 @@ ocr.selectText.7=\u0641\u0631\u0636 \u0627\u0644\u062A\u0639\u0631\u0641 \u0627\
ocr.selectText.8=\u0639\u0627\u062F\u064A (\u062E\u0637\u0623 \u0625\u0630\u0627 \u0643\u0627\u0646 PDF \u064A\u062D\u062A\u0648\u064A \u0639\u0644\u0649 \u0646\u0635) ocr.selectText.8=\u0639\u0627\u062F\u064A (\u062E\u0637\u0623 \u0625\u0630\u0627 \u0643\u0627\u0646 PDF \u064A\u062D\u062A\u0648\u064A \u0639\u0644\u0649 \u0646\u0635)
ocr.selectText.9=\u0625\u0639\u062F\u0627\u062F\u0627\u062A \u0625\u0636\u0627\u0641\u064A\u0629 ocr.selectText.9=\u0625\u0639\u062F\u0627\u062F\u0627\u062A \u0625\u0636\u0627\u0641\u064A\u0629
ocr.selectText.10=\u0648\u0636\u0639 \u0627\u0644\u062A\u0639\u0631\u0641 \u0627\u0644\u0636\u0648\u0626\u064A \u0639\u0644\u0649 \u0627\u0644\u062D\u0631\u0648\u0641 ocr.selectText.10=\u0648\u0636\u0639 \u0627\u0644\u062A\u0639\u0631\u0641 \u0627\u0644\u0636\u0648\u0626\u064A \u0639\u0644\u0649 \u0627\u0644\u062D\u0631\u0648\u0641
ocr.selectText.11 = إزالة الصور بعد التعرف الضوئي على الحروف (يزيل كل الصور ، يكون مفيدًا فقط إذا كان جزءًا من خطوة التحويل)
ocr.selectText.12 = نوع العرض (متقدم)
ocr.help=\u064A\u0631\u062C\u0649 \u0642\u0631\u0627\u0621\u0629 \u0647\u0630\u0647 \u0627\u0644\u0648\u062B\u0627\u0626\u0642 \u062D\u0648\u0644 \u0643\u064A\u0641\u064A\u0629 \u0627\u0633\u062A\u062E\u062F\u0627\u0645 \u0647\u0630\u0627 \u0644\u0644\u063A\u0627\u062A \u0623\u062E\u0631\u0649 \u0648 / \u0623\u0648 \u0627\u0644\u0627\u0633\u062A\u062E\u062F\u0627\u0645 \u0644\u064A\u0633 \u0641\u064A \u0639\u0627\u0645\u0644 \u0627\u0644\u0625\u0631\u0633\u0627\u0621 ocr.help=\u064A\u0631\u062C\u0649 \u0642\u0631\u0627\u0621\u0629 \u0647\u0630\u0647 \u0627\u0644\u0648\u062B\u0627\u0626\u0642 \u062D\u0648\u0644 \u0643\u064A\u0641\u064A\u0629 \u0627\u0633\u062A\u062E\u062F\u0627\u0645 \u0647\u0630\u0627 \u0644\u0644\u063A\u0627\u062A \u0623\u062E\u0631\u0649 \u0648 / \u0623\u0648 \u0627\u0644\u0627\u0633\u062A\u062E\u062F\u0627\u0645 \u0644\u064A\u0633 \u0641\u064A \u0639\u0627\u0645\u0644 \u0627\u0644\u0625\u0631\u0633\u0627\u0621
ocr.credit=\u062A\u0633\u062A\u062E\u062F\u0645 \u0647\u0630\u0647 \u0627\u0644\u062E\u062F\u0645\u0629 OCRmyPDF \u0648 Tesseract \u0644 OCR. ocr.credit=\u062A\u0633\u062A\u062E\u062F\u0645 \u0647\u0630\u0647 \u0627\u0644\u062E\u062F\u0645\u0629 OCRmyPDF \u0648 Tesseract \u0644 OCR.
ocr.submit=\u0645\u0639\u0627\u0644\u062C\u0629 PDF \u0628\u0627\u0633\u062A\u062E\u062F\u0627\u0645 OCR ocr.submit=\u0645\u0639\u0627\u0644\u062C\u0629 PDF \u0628\u0627\u0633\u062A\u062E\u062F\u0627\u0645 OCR
@ -182,8 +200,8 @@ pdfOrganiser.header=منظم صفحات PDF
pdfOrganiser.submit=إعادة ترتيب الصفحات pdfOrganiser.submit=إعادة ترتيب الصفحات
#multiTool #multiTool
multiTool.title = أداة متعددة PDF multiTool.title=أداة متعددة PDF
multiTool.header = أداة متعددة PDF multiTool.header=أداة متعددة PDF
#pageRemover #pageRemover
pageRemover.title=مزيل الصفحة pageRemover.title=مزيل الصفحة
@ -329,32 +347,32 @@ pdfToPDFA.credit=\u062A\u0633\u062A\u062E\u062F\u0645 \u0647\u0630\u0647 \u0627\
pdfToPDFA.submit=\u062A\u062D\u0648\u064A\u0644 pdfToPDFA.submit=\u062A\u062D\u0648\u064A\u0644
PDFToWord.title = تحويل PDF إلى Word PDFToWord.title=تحويل PDF إلى Word
PDFToWord.header = تحويل PDF إلى Word PDFToWord.header=تحويل PDF إلى Word
PDFToWord.selectText.1 = تنسيق ملف الإخراج PDFToWord.selectText.1=تنسيق ملف الإخراج
PDFToWord.credit = تستخدم هذه الخدمة LibreOffice لتحويل الملفات. PDFToWord.credit=تستخدم هذه الخدمة LibreOffice لتحويل الملفات.
PDFToWord.submit = تحويل PDFToWord.submit=تحويل
PDFToPresentation.title = PDF للعرض التقديمي PDFToPresentation.title=PDF للعرض التقديمي
PDFToPresentation.header = PDF للعرض التقديمي PDFToPresentation.header=PDF للعرض التقديمي
PDFToPresentation.selectText.1 = تنسيق ملف الإخراج PDFToPresentation.selectText.1=تنسيق ملف الإخراج
PDFToPresentation.credit = تستخدم هذه الخدمة LibreOffice لتحويل الملف. PDFToPresentation.credit=تستخدم هذه الخدمة LibreOffice لتحويل الملف.
PDFToPresentation.submit = تحويل PDFToPresentation.submit=تحويل
PDFToText.title = تحويل PDF إلى نص / RTF PDFToText.title=تحويل PDF إلى نص / RTF
PDFToText.header = تحويل PDF إلى نص / RTF PDFToText.header=تحويل PDF إلى نص / RTF
PDFToText.selectText.1 = تنسيق ملف الإخراج PDFToText.selectText.1=تنسيق ملف الإخراج
PDFToText.credit = تستخدم هذه الخدمة LibreOffice لتحويل الملفات. PDFToText.credit=تستخدم هذه الخدمة LibreOffice لتحويل الملفات.
PDFToText.submit = تحويل PDFToText.submit=تحويل
PDFToHTML.title = PDF إلى HTML PDFToHTML.title=PDF إلى HTML
PDFToHTML.header = PDF إلى HTML PDFToHTML.header=PDF إلى HTML
PDFToHTML.credit = تستخدم هذه الخدمة LibreOffice لتحويل الملفات. PDFToHTML.credit=تستخدم هذه الخدمة LibreOffice لتحويل الملفات.
PDFToHTML.submit = تحويل PDFToHTML.submit=تحويل
PDFToXML.title = تحويل PDF إلى XML PDFToXML.title=تحويل PDF إلى XML
PDFToXML.header = تحويل PDF إلى XML PDFToXML.header=تحويل PDF إلى XML
PDFToXML.credit = تستخدم هذه الخدمة LibreOffice لتحويل الملفات. PDFToXML.credit=تستخدم هذه الخدمة LibreOffice لتحويل الملفات.
PDFToXML.submit = تحويل PDFToXML.submit=تحويل

View file

@ -104,6 +104,20 @@ home.PDFToHTML.desc=PDF in HTML-Format konvertieren
home.PDFToXML.title=PDF in XML home.PDFToXML.title=PDF in XML
home.PDFToXML.desc=PDF in XML-Format konvertieren 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
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:
ScannerImageSplit.selectText.4=Bestimmt den Bereich der Farbvariation um die geschätzte Hintergrundfarbe herum (Standard: 30).
ScannerImageSplit.selectText.5=Mindestbereich:
ScannerImageSplit.selectText.6=Legt den minimalen Bereichsschwellenwert für ein Foto fest (Standard: 10000).
ScannerImageSplit.selectText.7=Minimaler Konturbereich:
ScannerImageSplit.selectText.8=Legt den minimalen Konturbereichsschwellenwert für ein Foto fest
ScannerImageSplit.selectText.9=Randgröße:
ScannerImageSplit.selectText.10=Legt die Größe des hinzugefügten und entfernten Randes fest, um weiße Ränder in der Ausgabe zu verhindern (Standard: 1).
navbar.settings=Einstellungen navbar.settings=Einstellungen
settings.title=Einstellungen settings.title=Einstellungen
@ -128,6 +142,8 @@ ocr.selectText.7=OCR erzwingen, OCR wird jede Seite entfernen und alle ursprüng
ocr.selectText.8=Normal (Fehler, wenn PDF Text enthält) ocr.selectText.8=Normal (Fehler, wenn PDF Text enthält)
ocr.selectText.9=Zusätzliche Einstellungen ocr.selectText.9=Zusätzliche Einstellungen
ocr.selectText.10=OCR-Modus ocr.selectText.10=OCR-Modus
ocr.selectText.11=Bilder nach OCR entfernen (Entfernt ALLE Bilder, nur sinnvoll, wenn Teil des Konvertierungsschritts)
ocr.selectText.12=Rendertyp (Erweitert)
ocr.help=Bitte lesen Sie diese Dokumentation, um zu erfahren, wie Sie dies für andere Sprachen verwenden und/oder nicht in Docker verwenden können ocr.help=Bitte lesen Sie diese Dokumentation, um zu erfahren, wie Sie dies für andere Sprachen verwenden und/oder nicht in Docker verwenden können
ocr.credit=Dieser Dienst verwendet OCRmyPDF und Tesseract für OCR. ocr.credit=Dieser Dienst verwendet OCRmyPDF und Tesseract für OCR.
ocr.submit=PDF mit OCR verarbeiten ocr.submit=PDF mit OCR verarbeiten

View file

@ -105,8 +105,22 @@ home.PDFToHTML.desc=Convert PDF to HTML format
home.PDFToXML.title=PDF to XML home.PDFToXML.title=PDF to XML
home.PDFToXML.desc=Convert PDF to XML format 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
ScannerImageSplit.selectText.1=Angle Threshold:
ScannerImageSplit.selectText.2=Sets the minimum absolute angle required for the image to be rotated (default: 10).
ScannerImageSplit.selectText.3=Tolerance:
ScannerImageSplit.selectText.4=Determines the range of color variation around the estimated background color (default: 30).
ScannerImageSplit.selectText.5=Minimum Area:
ScannerImageSplit.selectText.6=Sets the minimum area threshold for a photo (default: 10000).
ScannerImageSplit.selectText.7=Minimum Contour Area:
ScannerImageSplit.selectText.8=Sets the minimum contour area threshold for a photo
ScannerImageSplit.selectText.9=Border Size:
ScannerImageSplit.selectText.10=Sets the size of the border added and removed to prevent white borders in the output (default: 1).
navbar.settings=Settings navbar.settings=Settings
settings.title=Settings settings.title=Settings
settings.update=Update available settings.update=Update available
@ -120,6 +134,7 @@ settings.zipThreshold=Zip files when the number of downloaded files exceeds
#OCR #OCR
ocr.title=OCR / Scan Cleanup ocr.title=OCR / Scan Cleanup
ocr.header=Cleanup Scans / OCR (Optical Character Recognition) ocr.header=Cleanup Scans / OCR (Optical Character Recognition)
@ -133,6 +148,8 @@ ocr.selectText.7=Force OCR, will OCR Every page removing all original text eleme
ocr.selectText.8=Normal (Will error if PDF contains text) ocr.selectText.8=Normal (Will error if PDF contains text)
ocr.selectText.9=Additional Settings ocr.selectText.9=Additional Settings
ocr.selectText.10=OCR Mode ocr.selectText.10=OCR Mode
ocr.selectText.11=Remove images after OCR (Removes ALL images, only useful if part of conversion step)
ocr.selectText.12=Render Type (Advanced)
ocr.help=Please read this documentation on how to use this for other languages and/or use not in docker ocr.help=Please read this documentation on how to use this for other languages and/or use not in docker
ocr.credit=This service uses OCRmyPDF and Tesseract for OCR. ocr.credit=This service uses OCRmyPDF and Tesseract for OCR.
ocr.submit=Process PDF with OCR ocr.submit=Process PDF with OCR

View file

@ -104,6 +104,19 @@ home.PDFToHTML.desc=Convertir PDF a formato HTML
home.PDFToXML.title=PDF a XML home.PDFToXML.title=PDF a XML
home.PDFToXML.desc=Convertir PDF a formato XML 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
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:
ScannerImageSplit.selectText.4=Determina el rango de variación de color alrededor del color de fondo estimado (predeterminado: 30).
ScannerImageSplit.selectText.5=Área mínima:
ScannerImageSplit.selectText.6=Establece el umbral mínimo de área para una foto (predeterminado: 10000).
ScannerImageSplit.selectText.7=Área de contorno mínima:
ScannerImageSplit.selectText.8=Establece el umbral mínimo del área de contorno para una foto
ScannerImageSplit.selectText.9=Tamaño del borde:
ScannerImageSplit.selectText.10=Establece el tamaño del borde agregado y eliminado para evitar bordes blancos en la salida (predeterminado: 1).
navbar.settings=Ajustes navbar.settings=Ajustes
settings.title=Ajustes settings.title=Ajustes
@ -131,6 +144,8 @@ ocr.selectText.7=Fuerza OCR, OCR eliminará en cada página todo el texto origin
ocr.selectText.8=Normal (Se producirá un error si el PDF contiene texto) ocr.selectText.8=Normal (Se producirá un error si el PDF contiene texto)
ocr.selectText.9=Ajustes Adicionales ocr.selectText.9=Ajustes Adicionales
ocr.selectText.10=Modo OCR ocr.selectText.10=Modo OCR
ocr.selectText.11=Eliminar imágenes después de OCR (Elimina TODAS las imágenes, solo es útil si es parte del paso de conversión)
ocr.selectText.12=Tipo de procesamiento (avanzado)
ocr.help=Lea esta documentación sobre cómo usar esto para otros idiomas y/o no usarlo en docker ocr.help=Lea esta documentación sobre cómo usar esto para otros idiomas y/o no usarlo en docker
ocr.credit=Este servicio utiliza OCRmyPDF y Tesseract para OCR. ocr.credit=Este servicio utiliza OCRmyPDF y Tesseract para OCR.
ocr.submit=Procesa PDF con OCR ocr.submit=Procesa PDF con OCR

View file

@ -110,6 +110,20 @@ home.PDFToHTML.desc=Convertir le PDF au format HTML
home.PDFToXML.title=PDF vers XML home.PDFToXML.title=PDF vers XML
home.PDFToXML.desc=Convertir le PDF au format XML 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
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 :
ScannerImageSplit.selectText.4=Détermine la plage de variation de couleur autour de la couleur d'arrière-plan estimée (par défaut : 30).
ScannerImageSplit.selectText.5=Zone minimale :
ScannerImageSplit.selectText.6=Définit le seuil de zone minimum pour une photo (par défaut : 10000).
ScannerImageSplit.selectText.7=Zone de contour minimale :
ScannerImageSplit.selectText.8=Définit le seuil de zone de contour minimum pour une photo
ScannerImageSplit.selectText.9=Taille de la bordure :
ScannerImageSplit.selectText.10=Définit la taille de la bordure ajoutée et supprimée pour éviter les bordures blanches dans la sortie (par défaut : 1).
navbar.settings=Paramètres navbar.settings=Paramètres
settings.title=Paramètres settings.title=Paramètres
settings.update=Mise à jour disponible settings.update=Mise à jour disponible
@ -134,6 +148,8 @@ ocr.selectText.7=Forcer l'OCR, OCR chaque page supprimera tous les éléments de
ocr.selectText.8=Normal (Erreur si le PDF contient du texte) ocr.selectText.8=Normal (Erreur si le PDF contient du texte)
ocr.selectText.9=Paramètres supplémentaires ocr.selectText.9=Paramètres supplémentaires
ocr.selectText.10=Mode ROC ocr.selectText.10=Mode ROC
ocr.selectText.11=Supprimer les images après l'OCR (Supprime TOUTES les images, utile uniquement si elles font partie de l'étape de conversion)
ocr.selectText.12=Type de rendu (avancé)
ocr.help=Veuillez lire cette documentation pour savoir comment l'utiliser pour d'autres langues et/ou une utilisation non dans docker ocr.help=Veuillez lire cette documentation pour savoir comment l'utiliser pour d'autres langues et/ou une utilisation non dans docker
ocr.credit=Ce service utilise OCRmyPDF et Tesseract pour l'OCR. ocr.credit=Ce service utilise OCRmyPDF et Tesseract pour l'OCR.
ocr.submit=Traiter PDF avec OCR ocr.submit=Traiter PDF avec OCR

View file

@ -0,0 +1,35 @@
/* Rainbow Mode Styles */
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;
}
.dark-card {
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%) !important;
color: white !important;
}
.jumbotron {
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;
}
.list-group {
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%) !important;
color: fff !important;
}
.list-group-item {
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%) !important;
color: fff !important;
}
#support-section {
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%) !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;
}

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg version="1.1" id="Icons" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 32 32" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st1{fill:none;stroke:#000000;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:10;}
</style>
<path class="st0" d="M30,20H3v6c0,1.1,0.9,2,2,2h23c1.1,0,2-0.9,2-2V20z"/>
<line class="st0" x1="30" y1="20" x2="3" y2="4"/>
<line class="st0" x1="7" y1="24" x2="10" y2="24"/>
</svg>

After

Width:  |  Height:  |  Size: 691 B

View file

@ -0,0 +1,286 @@
function initializeGame() {
const gameContainer = document.getElementById('game-container');
const player = document.getElementById('player');
let playerSize = gameContainer.clientWidth * 0.0625; // 5% of container width
player.style.width = playerSize + 'px';
player.style.height = playerSize + 'px';
let playerX = gameContainer.clientWidth / 2 - playerSize / 2;
let playerY = gameContainer.clientHeight * 0.1;
const scoreElement = document.getElementById('score');
const levelElement = document.getElementById('level');
const livesElement = document.getElementById('lives');
const highScoreElement = document.getElementById('high-score');
let pdfSize = gameContainer.clientWidth * 0.0625; // 5% of container width
let projectileWidth = gameContainer.clientWidth * 0.00625; // 0.5% of container width
let projectileHeight = gameContainer.clientHeight * 0.01667; // 1% of container height
let paused = false;
const fireRate = 200; // Time between shots in milliseconds
let lastProjectileTime = 0;
let lives = 3;
let highScore = localStorage.getItem('highScore') ? parseInt(localStorage.getItem('highScore')) : 0;
updateHighScore();
const keysPressed = {};
const pdfs = [];
const projectiles = [];
let score = 0;
let level = 1;
let pdfSpeed = 1;
let gameOver = false;
function handleKeys() {
if (keysPressed['ArrowLeft']) {
playerX -= 10;
}
if (keysPressed['ArrowRight']) {
playerX += 10;
}
if (keysPressed[' '] && !gameOver) {
const currentTime = new Date().getTime();
if (currentTime - lastProjectileTime >= fireRate) {
shootProjectile();
lastProjectileTime = currentTime;
}
}
updatePlayerPosition();
}
document.addEventListener('keydown', (event) => {
if (event.key === ' ') {
event.preventDefault();
}
keysPressed[event.key] = true;
handleKeys();
});
document.addEventListener('keyup', (event) => {
keysPressed[event.key] = false;
});
function updatePlayerPosition() {
player.style.left = playerX + 'px';
player.style.bottom = playerY + 'px';
}
function updateLives() {
livesElement.textContent = 'Lives: ' + lives;
}
function updateHighScore() {
highScoreElement.textContent = 'High Score: ' + highScore;
}
function shootProjectile() {
const projectile = document.createElement('div');
projectile.classList.add('projectile');
projectile.style.backgroundColor = 'black';
projectile.style.width = projectileWidth + 'px';
projectile.style.height = projectileHeight + 'px';
projectile.style.left = (playerX + playerSize / 2 - projectileWidth / 2) + 'px';
projectile.style.top = (gameContainer.clientHeight - playerY - playerSize) + 'px';
gameContainer.appendChild(projectile);
projectiles.push(projectile);
}
function spawnPdf() {
const pdf = document.createElement('img');
pdf.src = 'images/file-earmark-pdf.svg';
pdf.classList.add('pdf');
pdf.style.width = pdfSize + 'px';
pdf.style.height = pdfSize + 'px';
pdf.style.left = Math.floor(Math.random() * (gameContainer.clientWidth - pdfSize)) + 'px';
pdf.style.top = '0px';
gameContainer.appendChild(pdf);
pdfs.push(pdf);
}
function resetEnemies() {
pdfs.forEach((pdf) => gameContainer.removeChild(pdf));
pdfs.length = 0;
}
function updateGame() {
if (gameOver || paused) return;
pdfs.forEach((pdf, pdfIndex) => {
const pdfY = parseInt(pdf.style.top) + pdfSpeed;
if (pdfY + 50 > gameContainer.clientHeight) {
gameContainer.removeChild(pdf);
pdfs.splice(pdfIndex, 1);
// Deduct 2 points when a PDF gets past the player
score -= 0;
updateScore();
// Decrease lives and check if game over
lives--;
updateLives();
if (lives <= 0) {
endGame();
return;
}
} else {
pdf.style.top = pdfY + 'px';
// Check for collision with player
if (collisionDetected(player, pdf)) {
lives--;
updateLives();
resetEnemies();
if (lives <= 0) {
endGame();
return;
}
}
}
});
projectiles.forEach((projectile, projectileIndex) => {
const projectileY = parseInt(projectile.style.top) - 10;
if (projectileY < 0) {
gameContainer.removeChild(projectile);
projectiles.splice(projectileIndex, 1);
} else {
projectile.style.top = projectileY + 'px';
}
for (let pdfIndex = 0; pdfIndex < pdfs.length; pdfIndex++) {
const pdf = pdfs[pdfIndex];
if (collisionDetected(projectile, pdf)) {
gameContainer.removeChild(pdf);
gameContainer.removeChild(projectile);
pdfs.splice(pdfIndex, 1);
projectiles.splice(projectileIndex, 1);
score = score + 10;
updateScore();
break;
}
}
});
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));
pdfs.length = 0;
projectiles.length = 0;
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();
}
function updateScore() {
scoreElement.textContent = 'Score: ' + score;
checkLevelUp();
}
function checkLevelUp() {
const newLevel = Math.floor(score / 100) + 1;
if (newLevel > level) {
level = newLevel;
levelElement.textContent = 'Level: ' + level;
pdfSpeed += 1;
}
}
function collisionDetected(a, b) {
const rectA = a.getBoundingClientRect();
const rectB = b.getBoundingClientRect();
return (
rectA.left < rectB.right &&
rectA.right > rectB.left &&
rectA.top < rectB.bottom &&
rectA.bottom > rectB.top
);
}
function endGame() {
gameOver = true;
if (score > highScore) {
highScore = score;
localStorage.setItem('highScore', highScore);
updateHighScore();
}
alert('Game Over! Your final score is: ' + score);
setTimeout(() => { // Wrap the resetGame() call in a setTimeout
resetGame();
}, 0);
}
let spawnPdfTimeout;
function spawnPdfInterval() {
console.log("spawnPdfInterval");
if (gameOver || paused) {
console.log("spawnPdfInterval 2");
clearTimeout(spawnPdfTimeout);
return;
}
console.log("spawnPdfInterval 3");
spawnPdf();
spawnPdfTimeout = setTimeout(spawnPdfInterval, 1000 - level * 50);
}
updatePlayerPosition();
updateGame();
spawnPdfInterval();
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
paused = true;
} else {
paused = false;
updateGame();
spawnPdfInterval();
}
});
}
window.initializeGame = initializeGame;

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-rainbow" viewBox="0 0 16 16">
<path d="M8 4.5a7 7 0 0 0-7 7 .5.5 0 0 1-1 0 8 8 0 1 1 16 0 .5.5 0 0 1-1 0 7 7 0 0 0-7-7zm0 2a5 5 0 0 0-5 5 .5.5 0 0 1-1 0 6 6 0 1 1 12 0 .5.5 0 0 1-1 0 5 5 0 0 0-5-5zm0 2a3 3 0 0 0-3 3 .5.5 0 0 1-1 0 4 4 0 1 1 8 0 .5.5 0 0 1-1 0 3 3 0 0 0-3-3zm0 2a1 1 0 0 0-1 1 .5.5 0 0 1-1 0 2 2 0 1 1 4 0 .5.5 0 0 1-1 0 1 1 0 0 0-1-1z"/>
</svg>

After

Width:  |  Height:  |  Size: 459 B

View file

@ -0,0 +1,22 @@
<!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='<3')}"></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">
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>

View file

@ -28,193 +28,235 @@
<!-- Custom --> <!-- Custom -->
<link rel="stylesheet" href="css/general.css"> <link rel="stylesheet" href="css/general.css">
<link rel="stylesheet" th:href="@{css/dark-mode.css}" id="dark-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">
<script> <script>
function toggleDarkMode() { var toggleCount = 0;
var darkModeStyles = document.getElementById("dark-mode-styles"); var lastToggleTime = Date.now();
var darkModeIcon = document.getElementById("dark-mode-icon");
if (localStorage.getItem("dark-mode") == "on") { function toggleDarkMode() {
localStorage.setItem("dark-mode", "off"); var currentTime = Date.now();
darkModeStyles.disabled = true; if (currentTime - lastToggleTime < 1000) {
darkModeIcon.src = "sun.svg"; toggleCount++;
} else { } else {
localStorage.setItem("dark-mode", "on"); toggleCount = 1;
darkModeStyles.disabled = false; }
darkModeIcon.src = "moon.svg"; lastToggleTime = currentTime;
}
} 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");
darkModeStyles.disabled = true;
rainbowModeStyles.disabled = false;
darkModeIcon.src = "rainbow.svg";
} else if (localStorage.getItem("dark-mode") == "on") {
localStorage.setItem("dark-mode", "off");
darkModeStyles.disabled = true;
rainbowModeStyles.disabled = true;
darkModeIcon.src = "sun.svg";
} else {
localStorage.setItem("dark-mode", "on");
darkModeStyles.disabled = false;
rainbowModeStyles.disabled = true;
darkModeIcon.src = "moon.svg";
}
}
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
var darkModeStyles = document.getElementById("dark-mode-styles"); var darkModeStyles = document.getElementById("dark-mode-styles");
var darkModeIcon = document.getElementById("dark-mode-icon"); var rainbowModeStyles = document.getElementById("rainbow-mode-styles");
var darkModeIcon = document.getElementById("dark-mode-icon");
// Check if the user has already set a preference if (localStorage.getItem("dark-mode") == "on") {
if (localStorage.getItem("dark-mode") == "on") { darkModeStyles.disabled = false;
darkModeStyles.disabled = false; rainbowModeStyles.disabled = true;
darkModeIcon.src = "moon.svg"; darkModeIcon.src = "moon.svg";
} else if (localStorage.getItem("dark-mode") == "off") { } else if (localStorage.getItem("dark-mode") == "off") {
darkModeStyles.disabled = true; darkModeStyles.disabled = true;
darkModeIcon.src = "sun.svg"; rainbowModeStyles.disabled = true;
} else { darkModeIcon.src = "sun.svg";
// Check the OS's default dark mode setting } else if (localStorage.getItem("dark-mode") == "rainbow") {
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { darkModeStyles.disabled = true;
darkModeStyles.disabled = false; rainbowModeStyles.disabled = false;
darkModeIcon.src = "moon.svg"; darkModeIcon.src = "rainbow.svg";
} else { } else {
darkModeStyles.disabled = true; if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
darkModeIcon.src = "sun.svg"; darkModeStyles.disabled = false;
} rainbowModeStyles.disabled = true;
} darkModeIcon.src = "moon.svg";
} else {
darkModeStyles.disabled = true;
rainbowModeStyles.disabled = true;
darkModeIcon.src = "sun.svg";
}
}
// Attach the toggleDarkMode function to the click event of the dark mode toggle document.getElementById("dark-mode-toggle").addEventListener("click", function (event) {
document.getElementById("dark-mode-toggle").addEventListener("click", function (event) { event.preventDefault();
event.preventDefault(); toggleDarkMode();
toggleDarkMode(); });
}); });
}); </script>
</script>
</head> </head>
<th:block th:fragment="fileSelector(name, multiple)" th:with="accept=${accept} ?: '*/*', inputText=${inputText} ?: #{pdfPrompt}"> <th:block th:fragment="fileSelector(name, multiple)" th:with="accept=${accept} ?: '*/*', inputText=${inputText} ?: #{pdfPrompt}">
<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 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>
<script>
$('form').submit(function(event) {
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 (override === 'multi' || (!multiple && files.length > 1) && override !== 'single' ) {
console.log("multi parallel download")
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]);
// 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 <script>
let filename = null;
const contentDispositionHeader = response.headers.get('Content-Disposition');
console.log(contentDispositionHeader)
if (contentDispositionHeader && contentDispositionHeader.indexOf('attachment') !== -1) {
filename = contentDispositionHeader.split('filename=')[1].replace(/"/g, '');
} else {
// If the Content-Disposition header is not present or does not contain the filename, use a default filename
filename = 'download';
}
console.log("filename=" + filename)
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 $(document).ready(function() {
if (downloadOption === 'sameWindow') {
console.log("same window")
// Open the file in the same window function loadGameScript(callback) {
window.location.href = URL.createObjectURL(blob); console.log('loadGameScript called');
} else if (downloadOption === 'newWindow') { const script = document.createElement('script');
console.log("new window") 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();
});
// Open the file in a new window
window.open(URL.createObjectURL(blob), '_blank');
} else {
console.log("else save")
// Download the file $('form').submit(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 (override === 'multi' || (!multiple && files.length > 1) && override !== 'single' ) {
console.log("multi parallel download")
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]);
// 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
let filename = null;
const contentDispositionHeader = response.headers.get('Content-Disposition');
console.log(contentDispositionHeader)
if (contentDispositionHeader && contentDispositionHeader.indexOf('attachment') !== -1) {
filename = decodeURIComponent(contentDispositionHeader.split('filename=')[1].replace(/"/g, ''));
} else {
// If the Content-Disposition header is not present or does not contain the filename, use a default filename
filename = 'download';
}
console.log("filename=" + filename)
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')) {
// 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'); const link = document.createElement('a');
link.href = URL.createObjectURL(blob); link.href = URL.createObjectURL(blob);
link.download = filename; link.download = filename;
link.click(); link.click();
} });
}); }
} else if (contentType.includes('json')) { })
// Handle the JSON response .catch(error => {
response.json().then(data => { console.log("error listener")
// Format the error message
const errorMessage = JSON.stringify(data, null, 2);
// Display the error message in an alert // Extract the error message and stack trace from the response
alert(`An error occurred: ${errorMessage}`); const errorMessage = error.message;
}); const stackTrace = error.stack;
} else { // Create an error message to display to the user
response.blob().then(blob => { const message = `${errorMessage}\n\n${stackTrace}`;
console.log("else save 2 zip")
// For ZIP files or other file types, just download the file $('#submitBtn').text(submitButtonText);
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 // Display the error message to the user
const errorMessage = error.message; alert(message);
const stackTrace = error.stack;
// 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);
});
}
$('#submitBtn').text(submitButtonText);
});
});
async function submitMultiPdfForm(event, url) { async function submitMultiPdfForm(event, url) {
// Get the selected PDF files // Get the selected PDF files
let files = $('#fileInput-input')[0].files; let files = $('#fileInput-input')[0].files;
@ -272,7 +314,7 @@ document.addEventListener("DOMContentLoaded", function () {
if (!contentDisposition) { if (!contentDisposition) {
//throw new Error('Content-Disposition header not found for file ' + i); //throw new Error('Content-Disposition header not found for file ' + i);
} else { } else {
fileName = contentDisposition.split('filename=')[1].replace(/"/g, ''); fileName = contentDisposition.split('filename=')[1].replace(/"/g, '');
} }
console.log('Received response for file ' + i + ': ' + response); console.log('Received response for file ' + i + ': ' + response);
@ -346,10 +388,10 @@ document.addEventListener("DOMContentLoaded", function () {
} }
} }
function updateProgressBar(progressBar, files) { function updateProgressBar(progressBar, files) {
let progress = ((progressBar.attr('aria-valuenow') / files.length) * 100) + (100 / files.length); let progress = ((progressBar.attr('aria-valuenow') / files.length) * 100) + (100 / files.length);
progressBar.css('width', progress + '%'); progressBar.css('width', progress + '%');
progressBar.attr('aria-valuenow', parseInt(progressBar.attr('aria-valuenow')) + 1); progressBar.attr('aria-valuenow', parseInt(progressBar.attr('aria-valuenow')) + 1);
} }
@ -357,6 +399,92 @@ document.addEventListener("DOMContentLoaded", function () {
</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 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>
<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"> <script th:inline="javascript">
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {

View file

@ -138,7 +138,7 @@ function compareVersions(version1, version2) {
<ul class="navbar-nav mr-auto flex-nowrap"> <ul class="navbar-nav mr-auto flex-nowrap">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#" th:href="@{multi-tool}" th:classappend="${currentPage}=='multi-tool' ? 'active' : ''"> <a class="nav-link" href="#" th:href="@{multi-tool}" th:classappend="${currentPage}=='multi-tool' ? 'active' : ''" th:title="#{home.multiTool.desc}">
<img class="icon" src="images/tools.svg" alt="icon"> <img class="icon" src="images/tools.svg" alt="icon">
<span class="icon-text" th:text="#{home.multiTool.title}"></span> <span class="icon-text" th:text="#{home.multiTool.title}"></span>
</a> </a>
@ -151,23 +151,23 @@ function compareVersions(version1, version2) {
</a> </a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown"> <div class="dropdown-menu" aria-labelledby="navbarDropdown">
<!-- Existing menu items --> <!-- Existing menu items -->
<a class="dropdown-item" href="#" th:href="@{merge-pdfs}" th:classappend="${currentPage}=='merge-pdfs' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{merge-pdfs}" th:classappend="${currentPage}=='merge-pdfs' ? 'active' : ''" th:title="#{home.merge.desc}">
<img class="icon" src="images/union.svg" alt="icon"> <img class="icon" src="images/union.svg" alt="icon">
<span class="icon-text" th:text="#{home.merge.title}"></span> <span class="icon-text" th:text="#{home.merge.title}"></span>
</a> </a>
<a class="dropdown-item" href="#" th:href="@{split-pdfs}" th:classappend="${currentPage}=='split-pdfs' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{split-pdfs}" th:classappend="${currentPage}=='split-pdfs' ? 'active' : ''" th:title="#{home.split.desc}">
<img class="icon" src="images/layout-split.svg" alt="icon"> <img class="icon" src="images/layout-split.svg" alt="icon">
<span class="icon-text" th:text="#{home.split.title}"></span> <span class="icon-text" th:text="#{home.split.title}"></span>
</a> </a>
<a class="dropdown-item" href="#" th:href="@{pdf-organizer}" th:classappend="${currentPage}=='pdf-organizer' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{pdf-organizer}" th:classappend="${currentPage}=='pdf-organizer' ? 'active' : ''" th:title="#{home.pdfOrganiser.desc}">
<img class="icon" src="images/sort-numeric-down.svg" alt="icon"> <img class="icon" src="images/sort-numeric-down.svg" alt="icon">
<span class="icon-text" th:text="#{home.pdfOrganiser.title}"></span> <span class="icon-text" th:text="#{home.pdfOrganiser.title}"></span>
</a> </a>
<a class="dropdown-item" href="#" th:href="@{rotate-pdf}" th:classappend="${currentPage}=='rotate-pdf' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{rotate-pdf}" th:classappend="${currentPage}=='rotate-pdf' ? 'active' : ''" th:title="#{home.rotate.desc}">
<img class="icon" src="images/arrow-clockwise.svg" alt="icon"> <img class="icon" src="images/arrow-clockwise.svg" alt="icon">
<span class="icon-text" th:text="#{home.rotate.title}"></span> <span class="icon-text" th:text="#{home.rotate.title}"></span>
</a> </a>
<a class="dropdown-item" href="#" th:href="@{remove-pages}" th:classappend="${currentPage}=='remove-pages' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{remove-pages}" th:classappend="${currentPage}=='remove-pages' ? 'active' : ''" th:title="#{home.removePages.desc}">
<img class="icon" src="images/file-earmark-x.svg" alt="icon"> <img class="icon" src="images/file-earmark-x.svg" alt="icon">
<span class="icon-text" th:text="#{home.removePages.title}"></span> <span class="icon-text" th:text="#{home.removePages.title}"></span>
</a> </a>
@ -181,51 +181,51 @@ function compareVersions(version1, version2) {
</a> </a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown"> <div class="dropdown-menu" aria-labelledby="navbarDropdown">
<!-- Existing menu items --> <!-- Existing menu items -->
<a class="dropdown-item" href="#" th:href="@{img-to-pdf}" th:classappend="${currentPage}=='img-to-pdf' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{img-to-pdf}" th:classappend="${currentPage}=='img-to-pdf' ? 'active' : ''" th:title="#{home.imageToPdf.desc}">
<img class="icon" src="images/image.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <img class="icon" src="images/image.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;">
<span class="icon-text" th:text="#{home.imageToPdf.title}"></span> <span class="icon-text" th:text="#{home.imageToPdf.title}"></span>
</a> </a>
<a class="dropdown-item" href="#" th:href="@{file-to-pdf}" th:classappend="${currentPage}=='file-to-pdf' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{file-to-pdf}" th:classappend="${currentPage}=='file-to-pdf' ? 'active' : ''" th:title="#{home.fileToPDF.desc}">
<img class="icon" src="images/file.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <img class="icon" src="images/file.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;">
<span class="icon-text" th:text="#{home.fileToPDF.title}"></span> <span class="icon-text" th:text="#{home.fileToPDF.title}"></span>
</a> </a>
<hr class="dropdown-divider"> <hr class="dropdown-divider">
<a class="dropdown-item" href="#" th:href="@{pdf-to-img}" th:classappend="${currentPage}=='pdf-to-img' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{pdf-to-img}" th:classappend="${currentPage}=='pdf-to-img' ? 'active' : ''" th:title="#{home.pdfToImage.desc}">
<img class="icon" src="images/image.svg" alt="icon"> <img class="icon" src="images/image.svg" alt="icon">
<span class="icon-text" th:text="#{home.pdfToImage.title}"></span> <span class="icon-text" th:text="#{home.pdfToImage.title}"></span>
</a> </a>
<a class="dropdown-item" href="#" th:href="@{pdf-to-word}" th:classappend="${currentPage}=='pdf-to-word' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{pdf-to-word}" th:classappend="${currentPage}=='pdf-to-word' ? 'active' : ''" th:title="#{home.PDFToWord.desc}">
<img class="icon" src="images/file-earmark-word.svg" alt="icon"> <img class="icon" src="images/file-earmark-word.svg" alt="icon">
<span class="icon-text" th:text="#{home.PDFToWord.title}"></span> <span class="icon-text" th:text="#{home.PDFToWord.title}"></span>
</a> </a>
<a class="dropdown-item" href="#" th:href="@{pdf-to-presentation}" th:classappend="${currentPage}=='pdf-to-presentation' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{pdf-to-presentation}" th:classappend="${currentPage}=='pdf-to-presentation' ? 'active' : ''" th:title="#{home.PDFToPresentation.desc}">
<img class="icon" src="images/file-earmark-ppt.svg" alt="icon"> <img class="icon" src="images/file-earmark-ppt.svg" alt="icon">
<span class="icon-text" th:text="#{home.PDFToPresentation.title}"></span> <span class="icon-text" th:text="#{home.PDFToPresentation.title}"></span>
</a> </a>
<a class="dropdown-item" href="#" th:href="@{pdf-to-text}" th:classappend="${currentPage}=='pdf-to-text' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{pdf-to-text}" th:classappend="${currentPage}=='pdf-to-text' ? 'active' : ''" th:title="#{home.PDFToText.desc}">
<img class="icon" src="images/filetype-txt.svg" alt="icon"> <img class="icon" src="images/filetype-txt.svg" alt="icon">
<span class="icon-text" th:text="#{home.PDFToText.title}"></span> <span class="icon-text" th:text="#{home.PDFToText.title}"></span>
</a> </a>
<a class="dropdown-item" href="#" th:href="@{pdf-to-html}" th:classappend="${currentPage}=='pdf-to-html' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{pdf-to-html}" th:classappend="${currentPage}=='pdf-to-html' ? 'active' : ''" th:title="#{home.PDFToHTML.desc}">
<img class="icon" src="images/filetype-html.svg" alt="icon"> <img class="icon" src="images/filetype-html.svg" alt="icon">
<span class="icon-text" th:text="#{home.PDFToHTML.title}"></span> <span class="icon-text" th:text="#{home.PDFToHTML.title}"></span>
</a> </a>
<a class="dropdown-item" href="#" th:href="@{pdf-to-xml}" th:classappend="${currentPage}=='pdf-to-xml' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{pdf-to-xml}" th:classappend="${currentPage}=='pdf-to-xml' ? 'active' : ''" th:title="#{home.PDFToXML.desc}">
<img class="icon" src="images/filetype-xml.svg" alt="icon"> <img class="icon" src="images/filetype-xml.svg" alt="icon">
<span class="icon-text" th:text="#{home.PDFToXML.title}"></span> <span class="icon-text" th:text="#{home.PDFToXML.title}"></span>
</a> </a>
<a class="dropdown-item" href="#" th:href="@{pdf-to-pdfa}" th:classappend="${currentPage}=='pdf-to-pdfa' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{pdf-to-pdfa}" th:classappend="${currentPage}=='pdf-to-pdfa' ? 'active' : ''" th:title="#{home.pdfToPDFA.desc}">
<img class="icon" src="images/file-earmark-pdf.svg" alt="icon"> <img class="icon" src="images/file-earmark-pdf.svg" alt="icon">
<span class="icon-text" th:text="#{home.pdfToPDFA.title}"></span> <span class="icon-text" th:text="#{home.pdfToPDFA.title}"></span>
@ -243,16 +243,16 @@ function compareVersions(version1, version2) {
<img class="icon" src="images/shield-check.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{navbar.security}"></span> <img class="icon" src="images/shield-check.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{navbar.security}"></span>
</a> </a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown"> <div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="#" th:href="@{add-password}" th:classappend="${currentPage}=='add-password' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{add-password}" th:classappend="${currentPage}=='add-password' ? 'active' : ''" th:title="#{home.addPassword.desc}">
<img class="icon" src="images/lock.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.addPassword.title}"></span> <img class="icon" src="images/lock.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.addPassword.title}"></span>
</a> </a>
<a class="dropdown-item" href="#" th:href="@{remove-password}" th:classappend="${currentPage}=='remove-password' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{remove-password}" th:classappend="${currentPage}=='remove-password' ? 'active' : ''" th:title="#{home.removePassword.desc}">
<img class="icon" src="images/unlock.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.removePassword.title}"></span> <img class="icon" src="images/unlock.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.removePassword.title}"></span>
</a> </a>
<a class="dropdown-item" href="#" th:href="@{change-permissions}" th:classappend="${currentPage}=='change-permissions' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{change-permissions}" th:classappend="${currentPage}=='change-permissions' ? 'active' : ''" th:title="#{home.permissions.desc}">
<img class="icon" src="images/shield-lock.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.permissions.title}"></span> <img class="icon" src="images/shield-lock.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.permissions.title}"></span>
</a> </a>
<a class="dropdown-item" href="#" th:href="@{add-watermark}" th:classappend="${currentPage}=='add-watermark' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{add-watermark}" th:classappend="${currentPage}=='add-watermark' ? 'active' : ''" th:title="#{home.watermark.desc}">
<img class="icon" src="images/droplet.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.watermark.title}"></span> <img class="icon" src="images/droplet.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.watermark.title}"></span>
</a> </a>
</div> </div>
@ -266,21 +266,24 @@ function compareVersions(version1, version2) {
</a> </a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown"> <div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="#" th:href="@{ocr-pdf}" th:classappend="${currentPage}=='ocr-pdf' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{ocr-pdf}" th:classappend="${currentPage}=='ocr-pdf' ? 'active' : ''" th:title="#{home.ocr.desc}">
<img class="icon" src="images/search.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.ocr.title}"></span> <img class="icon" src="images/search.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.ocr.title}"></span>
</a> </a>
<a class="dropdown-item" href="#" th:href="@{add-image}" th:classappend="${currentPage}=='add-image' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{add-image}" th:classappend="${currentPage}=='add-image' ? 'active' : ''" th:title="#{home.addImage.desc}">
<img class="icon" src="images/file-earmark-richtext.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.addImage.title}"></span> <img class="icon" src="images/file-earmark-richtext.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.addImage.title}"></span>
</a> </a>
<a class="dropdown-item" href="#" th:href="@{compress-pdf}" th:classappend="${currentPage}=='compress-pdf' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{compress-pdf}" th:classappend="${currentPage}=='compress-pdf' ? 'active' : ''" th:title="#{home.compressPdfs.desc}">
<img class="icon" src="images/file-zip.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.compressPdfs.title}"></span> <img class="icon" src="images/file-zip.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.compressPdfs.title}"></span>
</a> </a>
<a class="dropdown-item" href="#" th:href="@{extract-images}" th:classappend="${currentPage}=='extract-images' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{extract-images}" th:classappend="${currentPage}=='extract-images' ? 'active' : ''" th:title="#{home.extractImages.desc}">
<img class="icon" src="images/images.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.extractImages.title}"></span> <img class="icon" src="images/images.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.extractImages.title}"></span>
</a> </a>
<a class="dropdown-item" href="#" th:href="@{change-metadata}" th:classappend="${currentPage}=='change-metadata' ? 'active' : ''"> <a class="dropdown-item" href="#" th:href="@{change-metadata}" th:classappend="${currentPage}=='change-metadata' ? 'active' : ''" th:title="#{home.changeMetadata.desc}">
<img class="icon" src="images/clipboard-data.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.changeMetadata.title}"></span> <img class="icon" src="images/clipboard-data.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{home.changeMetadata.title}"></span>
</a> </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>
</div> </div>
</li> </li>
@ -364,6 +367,12 @@ function compareVersions(version1, version2) {
<input type="range" class="custom-range" min="1" max="9" step="1" id="zipThreshold" value="4"> <input type="range" class="custom-range" min="1" max="9" step="1" id="zipThreshold" value="4">
<span id="zipThresholdValue" class="ml-2"></span> <span id="zipThresholdValue" class="ml-2"></span>
</div> </div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="boredWaiting">
<label class="custom-control-label" for="boredWaiting">Bored Waiting? :)</label>
</div>
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal" th:text="#{close}"></button> <button type="button" class="btn btn-secondary" data-dismiss="modal" th:text="#{close}"></button>
@ -381,6 +390,7 @@ function compareVersions(version1, version2) {
// Set the selected option in the dropdown // Set the selected option in the dropdown
document.getElementById('downloadOption').value = downloadOption; document.getElementById('downloadOption').value = downloadOption;
// Save the selected option to local storage when the dropdown value changes // Save the selected option to local storage when the dropdown value changes
document.getElementById('downloadOption').addEventListener( document.getElementById('downloadOption').addEventListener(
'change', 'change',
@ -398,6 +408,8 @@ function compareVersions(version1, version2) {
document.getElementById('zipThreshold').value = zipThreshold; document.getElementById('zipThreshold').value = zipThreshold;
document.getElementById('zipThresholdValue').textContent = zipThreshold; document.getElementById('zipThresholdValue').textContent = zipThreshold;
// Save the selected value to local storage when the slider value changes // Save the selected value to local storage when the slider value changes
document.getElementById('zipThreshold').addEventListener('input', function () { document.getElementById('zipThreshold').addEventListener('input', function () {
zipThreshold = this.value; zipThreshold = this.value;
@ -405,6 +417,15 @@ function compareVersions(version1, version2) {
localStorage.setItem('zipThreshold', zipThreshold); localStorage.setItem('zipThreshold', zipThreshold);
}); });
var boredWaiting = localStorage.getItem('boredWaiting') || 'disabled';
document.getElementById('boredWaiting').checked = boredWaiting === 'enabled';
document.getElementById('boredWaiting').addEventListener('change', function() {
boredWaiting = this.checked ? 'enabled' : 'disabled';
localStorage.setItem('boredWaiting', boredWaiting);
});
</script> </script>

View file

@ -75,42 +75,39 @@ filter: invert(0.2) sepia(2) saturate(50) hue-rotate(190deg);
<!-- Features --> <!-- Features -->
<div class="features-container container"> <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.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.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(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.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.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(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.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.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(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.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.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(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.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.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(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.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.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(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.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.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(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.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.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.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>

View file

@ -0,0 +1,32 @@
<!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=#{extractImages.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="#{extractImages.header}"></h2>
<form id="multiPdfForm" th:action="@{adjust-contrast}" 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="contrastRange">Contrast</label>
<input name="contrastRange" type="range" class="form-control-range" id="contrastRange" min="-100" max="100" value="0" step="1">
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{extractImages.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>

View file

@ -0,0 +1,54 @@
<!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=#{home.ScannerImageSplit.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="#{home.ScannerImageSplit.title}"></h2>
<form id="multiPdfForm" th:action="@{extract-image-scans}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='image/*, application/pdf')}"></div>
<div class="form-group">
<label for="angleThreshold" th:text="#{ScannerImageSplit.selectText.1}"></label>
<input type="number" class="form-control" id="angleThreshold" name="angle_threshold" value="5">
<small id="angleThresholdHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.2}"></small>
</div>
<div class="form-group">
<label for="tolerance" th:text="#{ScannerImageSplit.selectText.3}"></label>
<input type="number" class="form-control" id="tolerance" name="tolerance" value="20">
<small id="toleranceHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.4}"></small>
</div>
<div class="form-group">
<label for="minArea" th:text="#{ScannerImageSplit.selectText.5}"></label>
<input type="number" class="form-control" id="minArea" name="min_area" value="8000">
<small id="minAreaHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.6}"></small>
</div>
<div class="form-group">
<label for="minContourArea" th:text="#{ScannerImageSplit.selectText.7}"></label>
<input type="number" class="form-control" id="minContourArea" name="min_contour_area" value="500">
<small id="minContourAreaHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.8}"></small>
</div>
<div class="form-group">
<label for="borderSize" th:text="#{ScannerImageSplit.selectText.9}"></label>
<input type="number" class="form-control" id="borderSize" name="border_size" value="1">
<small id="borderSizeHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.10}"></small>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{genericSubmit}"></button>
</form>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>

View file

@ -20,9 +20,9 @@
<label for="languages" class="form-label" th:text="#{ocr.selectText.1}"></label> <label for="languages" class="form-label" th:text="#{ocr.selectText.1}"></label>
<hr> <hr>
<div id="languages"> <div id="languages">
<div th:each="language, iterStat : ${languages}" > <div th:each="language, iterStat : ${languages}">
<input type="checkbox" class="form-check-input" th:name="languages" th:value="${language}" th:id="${'language-' + language}" /> <input type="checkbox" th:name="languages" th:value="${language}" th:id="${'language-' + language}" />
<label class="form-check-label" th:for="${'language-' + language}" th:text=" ${language}"></label> <label class="form-check-label" th:for="${'language-' + language}" th:text="${(language == 'eng') ? 'English' : language}"></label>
</div> </div>
</div> </div>
<hr> <hr>
@ -53,6 +53,19 @@
<input type="checkbox" class="form-check-input" name="clean-final" id="clean-final" /> <input type="checkbox" class="form-check-input" name="clean-final" id="clean-final" />
<label class="form-check-label" for="clean-final" th:text="#{ocr.selectText.5}"></label> <label class="form-check-label" for="clean-final" th:text="#{ocr.selectText.5}"></label>
</div> </div>
<div class="form-check">
<input type="checkbox" class="form-check-input" name="removeImagesAfter" id="removeImagesAfter" />
<label class="form-check-label" for="removeImagesAfter" th:text="#{ocr.selectText.11}"></label>
</div>
<div class="form-group">
<label th:text="#{ocr.selectText.12}"></label>
<select class="form-control" name="ocrRenderType">
<option value="hocr">HOCR (Latin/Roman alphabet only)</option>
<option value="sandwich">Sandwich</option>
</select>
</div>
<br> <br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{ocr.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{ocr.submit}"></button>
</form> </form>