Merge pull request #797 from Stirling-Tools/fixes

Fixes for docker changes plus others
This commit is contained in:
Anthony Stirling 2024-02-10 00:33:49 +00:00 committed by GitHub
commit 8a5883501a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 249 additions and 401 deletions

View file

@ -1,5 +1,32 @@
# Use the base image # Main stage
FROM frooodle/stirling-pdf-base:version8 FROM alpine:3.19.0
# JDK for app
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
apk add --no-cache \
ca-certificates \
tzdata \
tini \
bash \
curl \
openjdk17-jre \
# Doc conversion
libreoffice@testing \
# OCR MY PDF (unpaper for descew and other advanced featues)
ocrmypdf \
tesseract-ocr-data-eng \
# CV
py3-opencv \
# python3/pip
python3 && \
wget https://bootstrap.pypa.io/get-pip.py -qO - | python3 - --break-system-packages --no-cache-dir --upgrade && \
# uno unoconv and HTML
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint && \
mv /usr/share/tessdata /usr/share/tessdata-original
ARG VERSION_TAG ARG VERSION_TAG
@ -24,7 +51,7 @@ COPY build/libs/*.jar app.jar
## useradd -u $PUID -g stirlingpdfgroup -s /bin/sh stirlingpdfuser && \ ## useradd -u $PUID -g stirlingpdfgroup -s /bin/sh stirlingpdfuser && \
## mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME && \ ## mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME && \
# Set up necessary directories and permissions # Set up necessary directories and permissions
RUN mkdir /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \ RUN mkdir -p /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
##&& \ ##&& \
## chown -R stirlingpdfuser:stirlingpdfgroup /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /customFiles && \ ## chown -R stirlingpdfuser:stirlingpdfgroup /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /customFiles && \
## chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/tesseract-ocr-original && \ ## chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/tesseract-ocr-original && \

View file

@ -20,17 +20,19 @@ COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto
COPY build/libs/*.jar app.jar COPY build/libs/*.jar app.jar
RUN apk add --no-cache \ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
apk add --no-cache \
ca-certificates \ ca-certificates \
tzdata \ tzdata \
tini \ tini \
bash \ bash \
curl \ curl \
openjdk17-jre && \ openjdk17-jre \
# Doc conversion # Doc conversion
apk add --no-cache libreoffice --repository http://dl-cdn.alpinelinux.org/alpine/edge/community && \ libreoffice@testing \
# python and pip # python and pip
apk add --no-cache \
python3 && \ python3 && \
wget https://bootstrap.pypa.io/get-pip.py -qO - | python3 - --break-system-packages --no-cache-dir --upgrade && \ wget https://bootstrap.pypa.io/get-pip.py -qO - | python3 - --break-system-packages --no-cache-dir --upgrade && \
# uno unoconv and HTML # uno unoconv and HTML
@ -40,14 +42,11 @@ RUN apk add --no-cache \
# useradd -u $PUID -g stirlingpdfgroup -s /bin/sh stirlingpdfuser && \ # useradd -u $PUID -g stirlingpdfgroup -s /bin/sh stirlingpdfuser && \
# mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME # mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
# Set up necessary directories and permissions # Set up necessary directories and permissions
mkdir /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \ mkdir -p /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
# chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/fonts/opentype/noto /configs /customFiles # chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/fonts/opentype/noto /configs /customFiles
# Set font cache and permissions # Set font cache and permissions
fc-cache -f -v && \ fc-cache -f -v && \
chmod +x /scripts/*.sh && \ chmod +x /scripts/*.sh
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories
# chown stirlingpdfuser:stirlingpdfgroup /app.jar # chown stirlingpdfuser:stirlingpdfgroup /app.jar
# Set environment variables # Set environment variables

View file

@ -1,28 +0,0 @@
# Main stage
FROM alpine:3.19.0
# JDK for app
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
apk add --no-cache \
ca-certificates \
tzdata \
tini \
bash \
curl \
openjdk17-jre \
# Doc conversion
libreoffice@testing \
# OCR MY PDF (unpaper for descew and other advanced featues)
ocrmypdf \
tesseract-ocr-data-eng \
# CV
py3-opencv \
# python3/pip
python3 && \
wget https://bootstrap.pypa.io/get-pip.py -qO - | python3 - --break-system-packages --no-cache-dir --upgrade && \
# uno unoconv and HTML
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint && \
mv /usr/share/tessdata /usr/share/tessdata-original

View file

@ -77,16 +77,11 @@ public class AppConfig {
return Files.exists(Paths.get("/.dockerenv")); return Files.exists(Paths.get("/.dockerenv"));
} }
@Bean(name = "bookFormatsInstalled") @Bean(name = "bookAndHtmlFormatsInstalled")
public boolean bookFormatsInstalled() { public boolean bookAndHtmlFormatsInstalled() {
return applicationProperties.getSystem().getCustomApplications().isInstallBookFormats();
}
@Bean(name = "htmlFormatsInstalled")
public boolean htmlFormatsInstalled() {
return applicationProperties return applicationProperties
.getSystem() .getSystem()
.getCustomApplications() .getCustomApplications()
.isInstallAdvancedHtmlToPDF(); .isInstallBookAndHtmlFormats();
} }
} }

View file

@ -16,7 +16,7 @@ import org.springframework.stereotype.Service;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Service @Service
@DependsOn({"bookFormatsInstalled"}) @DependsOn({"bookAndHtmlFormatsInstalled"})
public class EndpointConfiguration { public class EndpointConfiguration {
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class); private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>(); private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
@ -24,14 +24,14 @@ public class EndpointConfiguration {
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private boolean bookFormatsInstalled; private boolean bookAndHtmlFormatsInstalled;
@Autowired @Autowired
public EndpointConfiguration( public EndpointConfiguration(
ApplicationProperties applicationProperties, ApplicationProperties applicationProperties,
@Qualifier("bookFormatsInstalled") boolean bookFormatsInstalled) { @Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) {
this.applicationProperties = applicationProperties; this.applicationProperties = applicationProperties;
this.bookFormatsInstalled = bookFormatsInstalled; this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
init(); init();
processEnvironmentConfigs(); processEnvironmentConfigs();
} }
@ -229,7 +229,7 @@ public class EndpointConfiguration {
private void processEnvironmentConfigs() { private void processEnvironmentConfigs() {
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove(); List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove(); List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
if (!bookFormatsInstalled) { if (!bookAndHtmlFormatsInstalled) {
groupsToRemove.add("Calibre"); groupsToRemove.add("Calibre");
} }
if (endpointsToRemove != null) { if (endpointsToRemove != null) {

View file

@ -26,12 +26,8 @@ public class PostStartupProcesses {
private boolean runningInDocker; private boolean runningInDocker;
@Autowired @Autowired
@Qualifier("bookFormatsInstalled") @Qualifier("bookAndHtmlFormatsInstalled")
private boolean bookFormatsInstalled; private boolean bookAndHtmlFormatsInstalled;
@Autowired
@Qualifier("htmlFormatsInstalled")
private boolean htmlFormatsInstalled;
private static final Logger logger = LoggerFactory.getLogger(PostStartupProcesses.class); private static final Logger logger = LoggerFactory.getLogger(PostStartupProcesses.class);
@ -39,34 +35,11 @@ public class PostStartupProcesses {
public void runInstallCommandBasedOnEnvironment() throws IOException, InterruptedException { public void runInstallCommandBasedOnEnvironment() throws IOException, InterruptedException {
List<List<String>> commands = new ArrayList<>(); List<List<String>> commands = new ArrayList<>();
// Checking for DOCKER_INSTALL_BOOK_FORMATS environment variable // Checking for DOCKER_INSTALL_BOOK_FORMATS environment variable
if (bookFormatsInstalled) { if (bookAndHtmlFormatsInstalled) {
List<String> tmpList = new ArrayList<>(); List<String> tmpList = new ArrayList<>();
// Set up the timezone configuration commands
tmpList.addAll(
Arrays.asList(
"sh",
"-c",
"echo 'tzdata tzdata/Areas select Europe' | debconf-set-selections; "
+ "echo 'tzdata tzdata/Zones/Europe select Berlin' | debconf-set-selections"));
commands.add(tmpList);
// Install calibre with DEBIAN_FRONTEND set to noninteractive
tmpList = new ArrayList<>(); tmpList = new ArrayList<>();
tmpList.addAll( tmpList.addAll(Arrays.asList("apk add --no-cache calibre"));
Arrays.asList(
"sh",
"-c",
"DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends calibre"));
commands.add(tmpList);
}
// Checking for DOCKER_INSTALL_HTML_FORMATS environment variable
if (htmlFormatsInstalled) {
List<String> tmpList = new ArrayList<>();
// Add -y flag for automatic yes to prompts and --no-install-recommends to reduce size
tmpList.addAll(
Arrays.asList(
"apt-get", "install", "wkhtmltopdf", "-y", "--no-install-recommends"));
commands.add(tmpList); commands.add(tmpList);
} }
@ -74,8 +47,6 @@ public class PostStartupProcesses {
// Run the command // Run the command
if (runningInDocker) { if (runningInDocker) {
List<String> tmpList = new ArrayList<>(); List<String> tmpList = new ArrayList<>();
tmpList.addAll(Arrays.asList("apt-get", "update"));
commands.add(0, tmpList);
for (List<String> list : commands) { for (List<String> list : commands) {
ProcessExecutorResult returnCode = ProcessExecutorResult returnCode =

View file

@ -23,21 +23,21 @@ import stirling.software.SPDF.utils.WebResponseUtils;
public class ConvertBookToPDFController { public class ConvertBookToPDFController {
@Autowired @Autowired
@Qualifier("bookFormatsInstalled") @Qualifier("bookAndHtmlFormatsInstalled")
private boolean bookFormatsInstalled; private boolean bookAndHtmlFormatsInstalled;
@PostMapping(consumes = "multipart/form-data", value = "/book/pdf") @PostMapping(consumes = "multipart/form-data", value = "/book/pdf")
@Operation( @Operation(
summary = summary =
"Convert a BOOK/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx) to PDF", "Convert a BOOK/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx) to PDF",
description = description =
"(Requires bookFormatsInstalled flag and Calibre installed) This endpoint takes an BOOK/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx) input and converts it to PDF format.") "(Requires bookAndHtmlFormatsInstalled flag and Calibre installed) This endpoint takes an BOOK/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx) input and converts it to PDF format.")
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute GeneralFile request) throws Exception { public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute GeneralFile request) throws Exception {
MultipartFile fileInput = request.getFileInput(); MultipartFile fileInput = request.getFileInput();
if (!bookFormatsInstalled) { if (!bookAndHtmlFormatsInstalled) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"bookFormatsInstalled flag is False, this functionality is not avaiable"); "bookAndHtmlFormatsInstalled flag is False, this functionality is not avaiable");
} }
if (fileInput == null) { if (fileInput == null) {

View file

@ -23,8 +23,8 @@ import stirling.software.SPDF.utils.WebResponseUtils;
public class ConvertHtmlToPDF { public class ConvertHtmlToPDF {
@Autowired @Autowired
@Qualifier("htmlFormatsInstalled") @Qualifier("bookAndHtmlFormatsInstalled")
private boolean htmlFormatsInstalled; private boolean bookAndHtmlFormatsInstalled;
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf") @PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
@Operation( @Operation(
@ -47,7 +47,10 @@ public class ConvertHtmlToPDF {
} }
byte[] pdfBytes = byte[] pdfBytes =
FileToPdf.convertHtmlToPdf( FileToPdf.convertHtmlToPdf(
request, fileInput.getBytes(), originalFilename, htmlFormatsInstalled); request,
fileInput.getBytes(),
originalFilename,
bookAndHtmlFormatsInstalled);
String outputFilename = String outputFilename =
originalFilename.replaceFirst("[.][^.]+$", "") originalFilename.replaceFirst("[.][^.]+$", "")

View file

@ -33,8 +33,8 @@ import stirling.software.SPDF.utils.WebResponseUtils;
public class ConvertMarkdownToPdf { public class ConvertMarkdownToPdf {
@Autowired @Autowired
@Qualifier("htmlFormatsInstalled") @Qualifier("bookAndHtmlFormatsInstalled")
private boolean htmlFormatsInstalled; private boolean bookAndHtmlFormatsInstalled;
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf") @PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
@Operation( @Operation(
@ -69,7 +69,10 @@ public class ConvertMarkdownToPdf {
byte[] pdfBytes = byte[] pdfBytes =
FileToPdf.convertHtmlToPdf( FileToPdf.convertHtmlToPdf(
null, htmlContent.getBytes(), "converted.html", htmlFormatsInstalled); null,
htmlContent.getBytes(),
"converted.html",
bookAndHtmlFormatsInstalled);
String outputFilename = String outputFilename =
originalFilename.replaceFirst("[.][^.]+$", "") originalFilename.replaceFirst("[.][^.]+$", "")

View file

@ -30,22 +30,22 @@ import stirling.software.SPDF.utils.WebResponseUtils;
public class ConvertPDFToBookController { public class ConvertPDFToBookController {
@Autowired @Autowired
@Qualifier("bookFormatsInstalled") @Qualifier("bookAndHtmlFormatsInstalled")
private boolean bookFormatsInstalled; private boolean bookAndHtmlFormatsInstalled;
@PostMapping(consumes = "multipart/form-data", value = "/pdf/book") @PostMapping(consumes = "multipart/form-data", value = "/pdf/book")
@Operation( @Operation(
summary = summary =
"Convert a PDF to a Book/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx .. (others to include by chatgpt) to PDF", "Convert a PDF to a Book/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx .. (others to include by chatgpt) to PDF",
description = description =
"(Requires bookFormatsInstalled flag and Calibre installed) This endpoint Convert a PDF to a Book/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx .. (others to include by chatgpt) to PDF") "(Requires bookAndHtmlFormatsInstalled flag and Calibre installed) This endpoint Convert a PDF to a Book/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx .. (others to include by chatgpt) to PDF")
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute PdfToBookRequest request) public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute PdfToBookRequest request)
throws Exception { throws Exception {
MultipartFile fileInput = request.getFileInput(); MultipartFile fileInput = request.getFileInput();
if (!bookFormatsInstalled) { if (!bookAndHtmlFormatsInstalled) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"bookFormatsInstalled flag is False, this functionality is not avaiable"); "bookAndHtmlFormatsInstalled flag is False, this functionality is not avaiable");
} }
if (fileInput == null) { if (fileInput == null) {

View file

@ -2,6 +2,10 @@ package stirling.software.SPDF.controller.api.converters;
import java.io.IOException; import java.io.IOException;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@ -9,6 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@ -17,6 +22,7 @@ import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest;
import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest; import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest;
import stirling.software.SPDF.model.api.converters.PdfToWordRequest; import stirling.software.SPDF.model.api.converters.PdfToWordRequest;
import stirling.software.SPDF.utils.PDFToFile; import stirling.software.SPDF.utils.PDFToFile;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/convert") @RequestMapping("/api/v1/convert")
@ -59,9 +65,21 @@ public class ConvertPDFToOffice {
throws IOException, InterruptedException { throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
String outputFormat = request.getOutputFormat(); String outputFormat = request.getOutputFormat();
if ("txt".equals(request.getOutputFormat())) {
PDFToFile pdfToFile = new PDFToFile(); try (PDDocument document = Loader.loadPDF(inputFile.getBytes())) {
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); PDFTextStripper stripper = new PDFTextStripper();
String text = stripper.getText(document);
return WebResponseUtils.bytesToWebResponse(
text.getBytes(),
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "")
+ ".txt",
MediaType.TEXT_PLAIN);
}
} else {
PDFToFile pdfToFile = new PDFToFile();
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
}
} }
@PostMapping(consumes = "multipart/form-data", value = "/pdf/word") @PostMapping(consumes = "multipart/form-data", value = "/pdf/word")

View file

@ -29,8 +29,8 @@ import stirling.software.SPDF.utils.WebResponseUtils;
public class ConvertWebsiteToPDF { public class ConvertWebsiteToPDF {
@Autowired @Autowired
@Qualifier("htmlFormatsInstalled") @Qualifier("bookAndHtmlFormatsInstalled")
private boolean htmlFormatsInstalled; private boolean bookAndHtmlFormatsInstalled;
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf") @PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
@Operation( @Operation(
@ -53,7 +53,7 @@ public class ConvertWebsiteToPDF {
// Prepare the OCRmyPDF command // Prepare the OCRmyPDF command
List<String> command = new ArrayList<>(); List<String> command = new ArrayList<>();
if (!htmlFormatsInstalled) { if (!bookAndHtmlFormatsInstalled) {
command.add("weasyprint"); command.add("weasyprint");
} else { } else {
command.add("wkhtmltopdf"); command.add("wkhtmltopdf");

View file

@ -2,23 +2,20 @@ package stirling.software.SPDF.controller.api.misc;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import javax.imageio.ImageIO;
import org.apache.pdfbox.Loader; import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageTree; import org.apache.pdfbox.pdmodel.PDPageTree;
import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.text.PDFTextStripper; import org.apache.pdfbox.text.PDFTextStripper;
import org.slf4j.Logger;
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.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@ -33,7 +30,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest; import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@ -41,6 +37,8 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class BlankPageController { public class BlankPageController {
private static final Logger logger = LoggerFactory.getLogger(BlankPageController.class);
@PostMapping(consumes = "multipart/form-data", value = "/remove-blanks") @PostMapping(consumes = "multipart/form-data", value = "/remove-blanks")
@Operation( @Operation(
summary = "Remove blank pages from a PDF file", summary = "Remove blank pages from a PDF file",
@ -63,63 +61,35 @@ public class BlankPageController {
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);
for (PDPage page : pages) { for (PDPage page : pages) {
System.out.println("checking page " + pageIndex); logger.info("checking page " + pageIndex);
textStripper.setStartPage(pageIndex + 1); textStripper.setStartPage(pageIndex + 1);
textStripper.setEndPage(pageIndex + 1); textStripper.setEndPage(pageIndex + 1);
String pageText = textStripper.getText(document); String pageText = textStripper.getText(document);
boolean hasText = !pageText.trim().isEmpty(); boolean hasText = !pageText.trim().isEmpty();
Boolean blank = false;
if (hasText) { if (hasText) {
pagesToKeepIndex.add(pageIndex); logger.info("page " + pageIndex + " has text, not blank");
System.out.println("page " + pageIndex + " has text"); blank = false;
} else { } else {
boolean hasImages = PdfUtils.hasImagesOnPage(page); boolean hasImages = PdfUtils.hasImagesOnPage(page);
if (hasImages) { if (hasImages) {
System.out.println("page " + pageIndex + " has image"); logger.info("page " + pageIndex + " has image, running blank detection");
Path tempFile = Files.createTempFile("image_", ".png");
// Render image and save as temp file // Render image and save as temp file
BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 300); BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 30);
ImageIO.write(image, "png", tempFile.toFile()); blank = isBlankImage(image, threshold, whitePercent, threshold);
List<String> command =
new ArrayList<>(
Arrays.asList(
"python",
System.getProperty("user.dir")
+ "/scripts/detect-blank-pages.py",
tempFile.toString(),
"--threshold",
String.valueOf(threshold),
"--white_percent",
String.valueOf(whitePercent)));
Boolean blank = false;
// Run CLI command
try {
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
.runCommandWithOutputHandling(command);
} catch (IOException e) {
// From detect-blank-pages.py
// Return code 1: The image is considered blank.
// Return code 0: The image is not considered blank.
// Since the process returned with a failure code, it should be blank.
blank = true;
}
if (blank) {
System.out.println("Skipping, Image was blank for page #" + pageIndex);
} else {
System.out.println(
"page " + pageIndex + " has image which is not blank");
pagesToKeepIndex.add(pageIndex);
}
} }
} }
if (blank) {
logger.info("Skipping, Image was blank for page #" + pageIndex);
} else {
logger.info("page " + pageIndex + " has image which is not blank");
pagesToKeepIndex.add(pageIndex);
}
pageIndex++; pageIndex++;
} }
System.out.print("pagesToKeep=" + pagesToKeepIndex.size());
// Remove pages not present in pagesToKeepIndex // Remove pages not present in pagesToKeepIndex
List<Integer> pageIndices = List<Integer> pageIndices =
IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList()); IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList());
@ -142,4 +112,30 @@ public class BlankPageController {
if (document != null) document.close(); if (document != null) document.close();
} }
} }
public static boolean isBlankImage(
BufferedImage image, int threshold, double whitePercent, int blurSize) {
if (image == null) {
logger.info("Error: Image is null");
return false;
}
// Convert to binary image based on the threshold
int whitePixels = 0;
int totalPixels = image.getWidth() * image.getHeight();
for (int i = 0; i < image.getHeight(); i++) {
for (int j = 0; j < image.getWidth(); j++) {
int color = image.getRGB(j, i) & 0xFF;
if (color >= 255 - threshold) {
whitePixels++;
}
}
}
double whitePixelPercentage = (whitePixels / (double) totalPixels) * 100;
logger.info(String.format("Page has white pixel percent of %.2f%%", whitePixelPercentage));
return whitePixelPercentage >= whitePercent;
}
} }

View file

@ -68,12 +68,14 @@ public class ShowJavascript {
if (script.isEmpty()) { if (script.isEmpty()) {
script = script =
"PDF '" + Filenames.toSimpleFileName(inputFile.getOriginalFilename()) + "' does not contain Javascript"; "PDF '"
+ Filenames.toSimpleFileName(inputFile.getOriginalFilename())
+ "' does not contain Javascript";
} }
return WebResponseUtils.bytesToWebResponse( return WebResponseUtils.bytesToWebResponse(
script.getBytes(StandardCharsets.UTF_8), script.getBytes(StandardCharsets.UTF_8),
inputFile.getOriginalFilename() + ".js"); Filenames.toSimpleFileName(inputFile.getOriginalFilename()) + ".js");
} }
} }
} }

View file

@ -7,6 +7,7 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.List;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
@ -87,49 +88,54 @@ public class StampController {
// Load the input PDF // Load the input PDF
PDDocument document = Loader.loadPDF(pdfFile.getBytes()); PDDocument document = Loader.loadPDF(pdfFile.getBytes());
for (PDPage page : document.getPages()) { List<Integer> pageNumbers = request.getPageNumbersList();
PDRectangle pageSize = page.getMediaBox();
float margin = marginFactor * (pageSize.getWidth() + pageSize.getHeight()) / 2;
PDPageContentStream contentStream = for (int pageIndex : pageNumbers) {
new PDPageContentStream( int zeroBasedIndex = pageIndex - 1;
document, page, PDPageContentStream.AppendMode.APPEND, true, true); if (zeroBasedIndex >= 0 && zeroBasedIndex < document.getNumberOfPages()) {
PDPage page = document.getPage(zeroBasedIndex);
PDRectangle pageSize = page.getMediaBox();
float margin = marginFactor * (pageSize.getWidth() + pageSize.getHeight()) / 2;
PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState(); PDPageContentStream contentStream =
graphicsState.setNonStrokingAlphaConstant(opacity); new PDPageContentStream(
contentStream.setGraphicsStateParameters(graphicsState); document, page, PDPageContentStream.AppendMode.APPEND, true, true);
if ("text".equalsIgnoreCase(watermarkType)) { PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
addTextStamp( graphicsState.setNonStrokingAlphaConstant(opacity);
contentStream, contentStream.setGraphicsStateParameters(graphicsState);
watermarkText,
document, if ("text".equalsIgnoreCase(watermarkType)) {
page, addTextStamp(
rotation, contentStream,
position, watermarkText,
fontSize, document,
alphabet, page,
overrideX, rotation,
overrideY, position,
margin, fontSize,
customColor); alphabet,
} else if ("image".equalsIgnoreCase(watermarkType)) { overrideX,
addImageStamp( overrideY,
contentStream, margin,
watermarkImage, customColor);
document, } else if ("image".equalsIgnoreCase(watermarkType)) {
page, addImageStamp(
rotation, contentStream,
position, watermarkImage,
fontSize, document,
overrideX, page,
overrideY, rotation,
margin); position,
fontSize,
overrideX,
overrideY,
margin);
}
contentStream.close();
} }
contentStream.close();
} }
return WebResponseUtils.pdfDocToWebResponse( return WebResponseUtils.pdfDocToWebResponse(
document, document,
Filenames.toSimpleFileName(pdfFile.getOriginalFilename()) Filenames.toSimpleFileName(pdfFile.getOriginalFilename())

View file

@ -13,7 +13,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
@Tag(name = "Convert", description = "Convert APIs") @Tag(name = "Convert", description = "Convert APIs")
public class ConverterWebController { public class ConverterWebController {
@ConditionalOnExpression("#{bookFormatsInstalled}") @ConditionalOnExpression("#{bookAndHtmlFormatsInstalled}")
@GetMapping("/book-to-pdf") @GetMapping("/book-to-pdf")
@Hidden @Hidden
public String convertBookToPdfForm(Model model) { public String convertBookToPdfForm(Model model) {
@ -21,7 +21,7 @@ public class ConverterWebController {
return "convert/book-to-pdf"; return "convert/book-to-pdf";
} }
@ConditionalOnExpression("#{bookFormatsInstalled}") @ConditionalOnExpression("#{bookAndHtmlFormatsInstalled}")
@GetMapping("/pdf-to-book") @GetMapping("/pdf-to-book")
@Hidden @Hidden
public String convertPdfToBookForm(Model model) { public String convertPdfToBookForm(Model model) {

View file

@ -290,31 +290,20 @@ public class ApplicationProperties {
} }
public static class CustomApplications { public static class CustomApplications {
private boolean installBookFormats; private boolean installBookAndHtmlFormats;
private boolean installAdvancedHtmlToPDF;
public boolean isInstallBookFormats() { public boolean isInstallBookAndHtmlFormats() {
return installBookFormats; return installBookAndHtmlFormats;
} }
public void setInstallBookFormats(boolean installBookFormats) { public void setInstallBookAndHtmlFormats(boolean installBookAndHtmlFormats) {
this.installBookFormats = installBookFormats; this.installBookAndHtmlFormats = installBookAndHtmlFormats;
}
public boolean isInstallAdvancedHtmlToPDF() {
return installAdvancedHtmlToPDF;
}
public void setInstallAdvancedHtmlToPDF(boolean installAdvancedHtmlToPDF) {
this.installAdvancedHtmlToPDF = installAdvancedHtmlToPDF;
} }
@Override @Override
public String toString() { public String toString() {
return "CustomApplications [installBookFormats=" return "CustomApplications [installBookAndHtmlFormats="
+ installBookFormats + installBookAndHtmlFormats
+ ", installAdvancedHtmlToPDF="
+ installAdvancedHtmlToPDF
+ "]"; + "]";
} }
} }

View file

@ -14,39 +14,4 @@ public class HTMLToPdfRequest extends PDFFile {
description = "Zoom level for displaying the website. Default is '1'.", description = "Zoom level for displaying the website. Default is '1'.",
defaultValue = "1") defaultValue = "1")
private float zoom; private float zoom;
@Schema(description = "Width of the page in centimeters.")
private Float pageWidth;
@Schema(description = "Height of the page in centimeters.")
private Float pageHeight;
@Schema(description = "Top margin of the page in millimeters.")
private Float marginTop;
@Schema(description = "Bottom margin of the page in millimeters.")
private Float marginBottom;
@Schema(description = "Left margin of the page in millimeters.")
private Float marginLeft;
@Schema(description = "Right margin of the page in millimeters.")
private Float marginRight;
@Schema(
description = "Enable or disable rendering of website background.",
allowableValues = {"Yes", "No"})
private String printBackground;
@Schema(
description =
"Enable or disable the default header. The default header includes the name of the page on the left and the page number on the right.",
allowableValues = {"Yes", "No"})
private String defaultHeader;
@Schema(
description = "Change the CSS media type of the page. Defaults to 'print'.",
allowableValues = {"none", "print", "screen"},
defaultValue = "print")
private String cssMediaType;
} }

View file

@ -6,11 +6,11 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFWithPageNums;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class AddStampRequest extends PDFFile { public class AddStampRequest extends PDFWithPageNums {
@Schema( @Schema(
description = "The stamp type (text or image)", description = "The stamp type (text or image)",

View file

@ -1,8 +1,9 @@
package stirling.software.SPDF.utils; package stirling.software.SPDF.utils;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
@ -34,95 +35,40 @@ public class FileToPdf {
tempInputFile = Files.createTempFile("input_", ".html"); tempInputFile = Files.createTempFile("input_", ".html");
Files.write(tempInputFile, fileBytes); Files.write(tempInputFile, fileBytes);
} else { } else {
tempInputFile = unzipAndGetMainHtml(fileBytes); tempInputFile = Files.createTempFile("input_", ".zip");
Files.write(tempInputFile, fileBytes);
} }
List<String> command = new ArrayList<>(); List<String> command = new ArrayList<>();
if (!htmlFormatsInstalled) { if (!htmlFormatsInstalled) {
command.add("weasyprint"); command.add("weasyprint");
} else { command.add(tempInputFile.toString());
command.add("wkhtmltopdf"); command.add(tempOutputFile.toString());
command.add("--enable-local-file-access");
command.add("--load-error-handling"); } else {
command.add("ignore"); command.add("ebook-convert");
command.add("--load-media-error-handling"); command.add(tempInputFile.toString());
command.add("ignore"); command.add(tempOutputFile.toString());
command.add("--zoom"); command.add("--paper-size");
command.add(String.valueOf(request.getZoom())); command.add("a4");
// if custom zoom add zoom style direct to html
// https://github.com/wkhtmltopdf/wkhtmltopdf/issues/4900
if (request.getZoom() != 1.0) { if (request.getZoom() != 1.0) {
String htmlContent = new String(Files.readAllBytes(tempInputFile)); // Create a temporary CSS file
File tempCssFile = Files.createTempFile("customStyle", ".css").toFile();
String zoomStyle = "<style>body { zoom: " + request.getZoom() + "; }</style>"; try (FileWriter writer = new FileWriter(tempCssFile)) {
// Check for <head> tag, add style tag to associated tag // Write the CSS rule to the file
if (htmlContent.contains("<head>")) { writer.write("body { zoom: " + request.getZoom() + "; }");
htmlContent = htmlContent.replace("<head>", "<head>" + zoomStyle);
} else if (htmlContent.contains("<html>")) {
// If no <head> tag, but <html> tag exists
htmlContent = htmlContent.replace("<html>", "<html>" + zoomStyle);
} else {
// If neither <head> nor <html> tags exist
htmlContent = zoomStyle + htmlContent;
} }
// rewrite new html to file command.add("--extra-css");
Files.write(tempInputFile, htmlContent.getBytes(StandardCharsets.UTF_8)); command.add(tempCssFile.getAbsolutePath());
}
if (request.getPageWidth() != null) {
command.add("--page-width");
command.add(request.getPageWidth() + "cm");
}
if (request.getPageHeight() != null) {
command.add("--page-height");
command.add(request.getPageHeight() + "cm");
}
if (request.getMarginTop() != null) {
command.add("--margin-top");
command.add(request.getMarginTop() + "mm");
}
// Repeat similar pattern for marginBottom, marginLeft, marginRight
if ("Yes".equalsIgnoreCase(request.getPrintBackground())) {
command.add("--background");
} else {
command.add("--no-background");
}
if ("Yes".equalsIgnoreCase(request.getDefaultHeader())) {
command.add("--default-header");
}
if ("print".equalsIgnoreCase(request.getCssMediaType())) {
command.add("--print-media-type");
} else if ("screen".equalsIgnoreCase(request.getCssMediaType())) {
command.add("--no-print-media-type");
} }
} }
command.add(tempInputFile.toString());
command.add(tempOutputFile.toString());
ProcessExecutorResult returnCode; ProcessExecutorResult returnCode;
if (fileName.endsWith(".zip")) {
if (htmlFormatsInstalled) { returnCode =
// command.add(1, "--allow"); ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
// command.add(2, tempInputFile.getParent().toString()); .runCommandWithOutputHandling(command);
}
returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
.runCommandWithOutputHandling(
command, tempInputFile.getParent().toFile());
} else {
returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
.runCommandWithOutputHandling(command);
}
pdfBytes = Files.readAllBytes(tempOutputFile); pdfBytes = Files.readAllBytes(tempOutputFile);
} catch (IOException e) { } catch (IOException e) {
@ -135,10 +81,6 @@ public class FileToPdf {
// Clean up temporary files // Clean up temporary files
Files.delete(tempOutputFile); Files.delete(tempOutputFile);
Files.delete(tempInputFile); Files.delete(tempInputFile);
if (fileName.endsWith(".zip")) {
GeneralUtils.deleteDirectory(tempInputFile.getParent());
}
} }
return pdfBytes; return pdfBytes;

View file

@ -12,6 +12,7 @@ import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor; import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@ -115,6 +116,13 @@ public class GeneralUtils {
} }
public static List<Integer> parsePageString(String pageOrder, int totalPages) { public static List<Integer> parsePageString(String pageOrder, int totalPages) {
if (pageOrder == null || pageOrder.isEmpty()) {
return Collections.singletonList(1);
}
if (pageOrder.matches("\\d+")) {
// Convert the single number string to an integer and return it in a list
return Collections.singletonList(Integer.parseInt(pageOrder));
}
return parsePageList(pageOrder.split(","), totalPages); return parsePageList(pageOrder.split(","), totalPages);
} }

View file

@ -14,8 +14,7 @@ system:
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
enableAlphaFunctionality: false # Set to enable functionality which might need more testing before it fully goes live (This feature might make no changes) enableAlphaFunctionality: false # Set to enable functionality which might need more testing before it fully goes live (This feature might make no changes)
customApplications: customApplications:
installBookFormats: false # Installs Calibre for book format conversion (For non docker it must be manually downloaded but will need to be true to show in UI) bookAndHtmlFormatsInstalled: false # Installs Calibre for book format conversion (For non docker it must be manually downloaded but will need to be true to show in UI)
installAdvancedHtmlToPDF: false # DO NOT USE EXTERNALLY, NOT SAFE! Install wkHtmlToPDF (For non docker it must be manually downloaded but will need to be true to show in UI)
#ui: #ui:
# appName: exampleAppName # Application's visible name # appName: exampleAppName # Application's visible name

View file

@ -14,13 +14,16 @@ function initializeGame() {
const highScoreElement = document.getElementById('high-score'); const highScoreElement = document.getElementById('high-score');
let pdfSize = gameContainer.clientWidth * 0.0625; // 5% of container width let pdfSize = gameContainer.clientWidth * 0.0625; // 5% of container width
let projectileWidth = gameContainer.clientWidth * 0.00625; // 0.5% of container width let projectileWidth = gameContainer.clientWidth * 0.00625;// 0.00625; // 0.5% of container width
let projectileHeight = gameContainer.clientHeight * 0.01667; // 1% of container height let projectileHeight = gameContainer.clientHeight * 0.01667; // 1% of container height
let paused = false; let paused = false;
const fireRate = 200; // Time between shots in milliseconds const fireRate = 200; // Time between shots in milliseconds
let lastProjectileTime = 0; let lastProjectileTime = 0;
let lives = 3; let lives = 3;
let highScore = localStorage.getItem('highScore') ? parseInt(localStorage.getItem('highScore')) : 0; let highScore = localStorage.getItem('highScore') ? parseInt(localStorage.getItem('highScore')) : 0;
updateHighScore(); updateHighScore();
@ -31,7 +34,7 @@ function initializeGame() {
const projectiles = []; const projectiles = [];
let score = 0; let score = 0;
let level = 1; let level = 1;
let pdfSpeed = 1; let pdfSpeed = 0.5;
let gameOver = false; let gameOver = false;
function handleKeys() { function handleKeys() {
@ -119,7 +122,7 @@ function initializeGame() {
for (let pdfIndex = 0; pdfIndex < pdfs.length; pdfIndex++) { for (let pdfIndex = 0; pdfIndex < pdfs.length; pdfIndex++) {
const pdf = pdfs[pdfIndex]; const pdf = pdfs[pdfIndex];
const pdfY = parseInt(pdf.style.top) + pdfSpeed; const pdfY = parseFloat(pdf.style.top) + pdfSpeed;
if (pdfY + 50 > gameContainer.clientHeight) { if (pdfY + 50 > gameContainer.clientHeight) {
gameContainer.removeChild(pdf); gameContainer.removeChild(pdf);
pdfs.splice(pdfIndex, 1); pdfs.splice(pdfIndex, 1);
@ -218,7 +221,7 @@ function initializeGame() {
if (newLevel > level) { if (newLevel > level) {
level = newLevel; level = newLevel;
levelElement.textContent = 'Level: ' + level; levelElement.textContent = 'Level: ' + level;
pdfSpeed += 1; pdfSpeed += 0.2;
} }
} }
@ -249,6 +252,10 @@ function initializeGame() {
let spawnPdfTimeout; let spawnPdfTimeout;
const BASE_SPAWN_INTERVAL_MS = 1250; // milliseconds before a new enemy spawns
const LEVEL_INCREASE_FACTOR_MS = 0; // milliseconds to decrease the spawn interval per level
const MAX_SPAWN_RATE_REDUCTION_MS = 800; // Max milliseconds from the base spawn interval
function spawnPdfInterval() { function spawnPdfInterval() {
console.log("spawnPdfInterval"); console.log("spawnPdfInterval");
if (gameOver || paused) { if (gameOver || paused) {
@ -258,7 +265,9 @@ function initializeGame() {
} }
console.log("spawnPdfInterval 3"); console.log("spawnPdfInterval 3");
spawnPdf(); spawnPdf();
spawnPdfTimeout = setTimeout(spawnPdfInterval, 1000 - level * 50); let spawnRateReduction = Math.min(level * LEVEL_INCREASE_FACTOR_MS, MAX_SPAWN_RATE_REDUCTION_MS);
let spawnRate = BASE_SPAWN_INTERVAL_MS - spawnRateReduction;
spawnPdfTimeout = setTimeout(spawnPdfInterval, spawnRate);
} }
updatePlayerPosition(); updatePlayerPosition();

View file

@ -20,63 +20,6 @@
<input type="number" step="0.1" class="form-control" id="zoom" name="zoom" value="1" /> <input type="number" step="0.1" class="form-control" id="zoom" name="zoom" value="1" />
</div> </div>
<div class="mb-3">
<label for="pageWidth" th:text="#{HTMLToPDF.pageWidth}" class="form-label"></label>
<input type="number" class="form-control" id="pageWidth" name="pageWidth" />
</div>
<div class="mb-3">
<label for="pageHeight" th:text="#{HTMLToPDF.pageHeight}" class="form-label"></label>
<input type="number" class="form-control" id="pageHeight" name="pageHeight" />
</div>
<div class="mb-3">
<label for="marginTop" th:text="#{HTMLToPDF.marginTop}" class="form-label"></label>
<input type="number" class="form-control" id="marginTop" name="marginTop" />
</div>
<div class="mb-3">
<label for="marginBottom" th:text="#{HTMLToPDF.marginBottom}" class="form-label"></label>
<input type="number" class="form-control" id="marginBottom" name="marginBottom" />
</div>
<div class="mb-3">
<label for="marginLeft" th:text="#{HTMLToPDF.marginLeft}" class="form-label"></label>
<input type="number" class="form-control" id="marginLeft" name="marginLeft" />
</div>
<div class="mb-3">
<label for="marginRight" th:text="#{HTMLToPDF.marginRight}" class="form-label"></label>
<input type="number" class="form-control" id="marginRight" name="marginRight" />
</div>
<div class="mb-3">
<label th:text="#{HTMLToPDF.printBackground}" class="form-label"></label>
<select class="form-select" name="printBackground">
<option value="Yes" th:text="#{yes}">Yes</option>
<option value="No" th:text="#{no}">No</option>
</select>
</div>
<div class="mb-3">
<label th:text="#{HTMLToPDF.defaultHeader}" class="form-label"></label>
<select class="form-select" name="defaultHeader">
<option value="No" th:text="#{no}">No</option>
<option value="Yes" th:text="#{yes}">Yes</option>
</select>
</div>
<div class="mb-3">
<label th:text="#{HTMLToPDF.cssMediaType}" class="form-label"></label>
<select class="form-select" name="cssMediaType">
<option value="screen" th:text="#{HTMLToPDF.screen}">Screen</option>
<option value="none" th:text="#{HTMLToPDF.none}">None</option>
<option value="print" th:text="#{HTMLToPDF.print}">Print</option>
</select>
</div>
<br> <br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{HTMLToPDF.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{HTMLToPDF.submit}"></button>

View file

@ -19,6 +19,7 @@
<label th:text="#{PDFToText.selectText.1}"></label> <label th:text="#{PDFToText.selectText.1}"></label>
<select class="form-control" name="outputFormat"> <select class="form-control" name="outputFormat">
<option value="rtf">RTF</option> <option value="rtf">RTF</option>
<option value="txt">TXT</option>
</select> </select>
</div> </div>
<br> <br>