Merge pull request #682 from Stirling-Tools/ebook

Ebook
This commit is contained in:
Anthony Stirling 2024-01-10 00:41:54 +00:00 committed by GitHub
commit 50ee829e5f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 652 additions and 48 deletions

View file

@ -12,7 +12,7 @@ plugins {
import com.github.jk1.license.render.*
group = 'stirling.software'
version = '0.19.0'
version = '0.19.1'
sourceCompatibility = '17'
repositories {

View file

@ -1,5 +1,8 @@
package stirling.software.SPDF.config;
import java.nio.file.Files;
import java.nio.file.Paths;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -57,4 +60,22 @@ public class AppConfig {
if (appName == null) appName = System.getenv("rateLimit");
return (appName != null) ? Boolean.valueOf(appName) : false;
}
@Bean(name = "RunningInDocker")
public boolean runningInDocker() {
return Files.exists(Paths.get("/.dockerenv"));
}
@Bean(name = "bookFormatsInstalled")
public boolean bookFormatsInstalled() {
return applicationProperties.getSystem().getCustomApplications().isInstallBookFormats();
}
@Bean(name = "htmlFormatsInstalled")
public boolean htmlFormatsInstalled() {
return applicationProperties
.getSystem()
.getCustomApplications()
.isInstallAdvancedHtmlToPDF();
}
}

View file

@ -9,11 +9,14 @@ import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Service;
import stirling.software.SPDF.model.ApplicationProperties;
@Service
@DependsOn({"bookFormatsInstalled"})
public class EndpointConfiguration {
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
@ -21,9 +24,14 @@ public class EndpointConfiguration {
private final ApplicationProperties applicationProperties;
private boolean bookFormatsInstalled;
@Autowired
public EndpointConfiguration(ApplicationProperties applicationProperties) {
public EndpointConfiguration(
ApplicationProperties applicationProperties,
@Qualifier("bookFormatsInstalled") boolean bookFormatsInstalled) {
this.applicationProperties = applicationProperties;
this.bookFormatsInstalled = bookFormatsInstalled;
init();
processEnvironmentConfigs();
}
@ -145,6 +153,12 @@ public class EndpointConfiguration {
addEndpointToGroup("CLI", "ocr-pdf");
addEndpointToGroup("CLI", "html-to-pdf");
addEndpointToGroup("CLI", "url-to-pdf");
addEndpointToGroup("CLI", "book-to-pdf");
addEndpointToGroup("CLI", "pdf-to-book");
// Calibre
addEndpointToGroup("Calibre", "book-to-pdf");
addEndpointToGroup("Calibre", "pdf-to-book");
// python
addEndpointToGroup("Python", "extract-image-scans");
@ -215,7 +229,9 @@ public class EndpointConfiguration {
private void processEnvironmentConfigs() {
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
if (!bookFormatsInstalled) {
groupsToRemove.add("Calibre");
}
if (endpointsToRemove != null) {
for (String endpoint : endpointsToRemove) {
disableEndpoint(endpoint.trim());

View file

@ -0,0 +1,98 @@
package stirling.software.SPDF.config;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@Component
public class PostStartupProcesses {
@Autowired ApplicationProperties applicationProperties;
@Autowired
@Qualifier("RunningInDocker")
private boolean runningInDocker;
@Autowired
@Qualifier("bookFormatsInstalled")
private boolean bookFormatsInstalled;
@Autowired
@Qualifier("htmlFormatsInstalled")
private boolean htmlFormatsInstalled;
private static final Logger logger = LoggerFactory.getLogger(PostStartupProcesses.class);
@PostConstruct
public void runInstallCommandBasedOnEnvironment() throws IOException, InterruptedException {
List<List<String>> commands = new ArrayList<>();
// Checking for DOCKER_INSTALL_BOOK_FORMATS environment variable
if (bookFormatsInstalled) {
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.addAll(
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);
}
if (!commands.isEmpty()) {
// Run the command
if (runningInDocker) {
List<String> tmpList = new ArrayList<>();
tmpList.addAll(Arrays.asList("apt-get", "update"));
commands.add(0, tmpList);
for (List<String> list : commands) {
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.INSTALL_APP, true)
.runCommandWithOutputHandling(list);
logger.info("RC for app installs {}", returnCode.getRc());
}
} else {
logger.info(
"Not running inside Docker so skipping automated install process with command.");
}
} else {
if (runningInDocker) {
logger.info("No custom apps to install.");
}
}
}
}

View file

@ -0,0 +1,68 @@
package stirling.software.SPDF.controller.api.converters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.GeneralFile;
import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
@RequestMapping("/api/v1/convert")
public class ConvertBookToPDFController {
@Autowired
@Qualifier("bookFormatsInstalled")
private boolean bookFormatsInstalled;
@PostMapping(consumes = "multipart/form-data", value = "/book/pdf")
@Operation(
summary =
"Convert a BOOK/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx) to PDF",
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.")
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute GeneralFile request) throws Exception {
MultipartFile fileInput = request.getFileInput();
if (!bookFormatsInstalled) {
throw new IllegalArgumentException(
"bookFormatsInstalled flag is False, this functionality is not avaiable");
}
if (fileInput == null) {
throw new IllegalArgumentException("Please provide a file for conversion.");
}
String originalFilename = fileInput.getOriginalFilename();
if (originalFilename != null) {
String originalFilenameLower = originalFilename.toLowerCase();
if (!originalFilenameLower.endsWith(".epub")
&& !originalFilenameLower.endsWith(".mobi")
&& !originalFilenameLower.endsWith(".azw3")
&& !originalFilenameLower.endsWith(".fb2")
&& !originalFilenameLower.endsWith(".txt")
&& !originalFilenameLower.endsWith(".docx")) {
throw new IllegalArgumentException(
"File must be in .epub, .mobi, .azw3, .fb2, .txt, or .docx format.");
}
}
byte[] pdfBytes = FileToPdf.convertBookTypeToPdf(fileInput.getBytes(), originalFilename);
String outputFilename =
originalFilename.replaceFirst("[.][^.]+$", "")
+ ".pdf"; // Remove file extension and append .pdf
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
}
}

View file

@ -1,5 +1,7 @@
package stirling.software.SPDF.controller.api.converters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@ -19,6 +21,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RequestMapping("/api/v1/convert")
public class ConvertHtmlToPDF {
@Autowired
@Qualifier("htmlFormatsInstalled")
private boolean htmlFormatsInstalled;
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
@Operation(
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
@ -37,7 +43,9 @@ public class ConvertHtmlToPDF {
|| (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) {
throw new IllegalArgumentException("File must be either .html or .zip format.");
}
byte[] pdfBytes = FileToPdf.convertHtmlToPdf(fileInput.getBytes(), originalFilename);
byte[] pdfBytes =
FileToPdf.convertHtmlToPdf(
fileInput.getBytes(), originalFilename, htmlFormatsInstalled);
String outputFilename =
originalFilename.replaceFirst("[.][^.]+$", "")

View file

@ -3,6 +3,8 @@ package stirling.software.SPDF.controller.api.converters;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@ -22,6 +24,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RequestMapping("/api/v1/convert")
public class ConvertMarkdownToPdf {
@Autowired
@Qualifier("htmlFormatsInstalled")
private boolean htmlFormatsInstalled;
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
@Operation(
summary = "Convert a Markdown file to PDF",
@ -46,7 +52,9 @@ public class ConvertMarkdownToPdf {
HtmlRenderer renderer = HtmlRenderer.builder().build();
String htmlContent = renderer.render(document);
byte[] pdfBytes = FileToPdf.convertHtmlToPdf(htmlContent.getBytes(), "converted.html");
byte[] pdfBytes =
FileToPdf.convertHtmlToPdf(
htmlContent.getBytes(), "converted.html", htmlFormatsInstalled);
String outputFilename =
originalFilename.replaceFirst("[.][^.]+$", "")

View file

@ -0,0 +1,101 @@
package stirling.software.SPDF.controller.api.converters;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.converters.PdfToBookRequest;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Convert", description = "Convert APIs")
@RequestMapping("/api/v1/convert")
public class ConvertPDFToBookController {
@Autowired
@Qualifier("bookFormatsInstalled")
private boolean bookFormatsInstalled;
@PostMapping(consumes = "multipart/form-data", value = "/pdf/book")
@Operation(
summary =
"Convert a PDF to a Book/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx .. (others to include by chatgpt) to PDF",
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")
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute PdfToBookRequest request)
throws Exception {
MultipartFile fileInput = request.getFileInput();
if (!bookFormatsInstalled) {
throw new IllegalArgumentException(
"bookFormatsInstalled flag is False, this functionality is not avaiable");
}
if (fileInput == null) {
throw new IllegalArgumentException("Please provide a file for conversion.");
}
// Validate the output format
String outputFormat = request.getOutputFormat().toLowerCase();
List<String> allowedFormats =
Arrays.asList(
"epub", "mobi", "azw3", "docx", "rtf", "txt", "html", "lit", "fb2", "pdb",
"lrf");
if (!allowedFormats.contains(outputFormat)) {
throw new IllegalArgumentException("Invalid output format: " + outputFormat);
}
byte[] outputFileBytes;
List<String> command = new ArrayList<>();
Path tempOutputFile =
Files.createTempFile(
"output_",
"." + outputFormat); // Use the output format for the file extension
Path tempInputFile = null;
try {
// Create temp input file from the provided PDF
tempInputFile = Files.createTempFile("input_", ".pdf"); // Assuming input is always PDF
Files.write(tempInputFile, fileInput.getBytes());
command.add("ebook-convert");
command.add(tempInputFile.toString());
command.add(tempOutputFile.toString());
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.CALIBRE)
.runCommandWithOutputHandling(command);
outputFileBytes = Files.readAllBytes(tempOutputFile);
} finally {
// Clean up temporary files
if (tempInputFile != null) {
Files.deleteIfExists(tempInputFile);
}
Files.deleteIfExists(tempOutputFile);
}
String outputFilename =
fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "."
+ outputFormat; // Remove file extension and append .pdf
return WebResponseUtils.bytesToWebResponse(outputFileBytes, outputFilename);
}
}

View file

@ -6,6 +6,8 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@ -26,6 +28,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RequestMapping("/api/v1/convert")
public class ConvertWebsiteToPDF {
@Autowired
@Qualifier("htmlFormatsInstalled")
private boolean htmlFormatsInstalled;
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
@Operation(
summary = "Convert a URL to a PDF",
@ -47,7 +53,11 @@ public class ConvertWebsiteToPDF {
// Prepare the OCRmyPDF command
List<String> command = new ArrayList<>();
if (!htmlFormatsInstalled) {
command.add("weasyprint");
} else {
command.add("wkhtmltopdf");
}
command.add(URL);
command.add(tempOutputFile.toString());

View file

@ -1,5 +1,6 @@
package stirling.software.SPDF.controller.web;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@ -12,6 +13,22 @@ import io.swagger.v3.oas.annotations.tags.Tag;
@Tag(name = "Convert", description = "Convert APIs")
public class ConverterWebController {
@ConditionalOnExpression("#{bookFormatsInstalled}")
@GetMapping("/book-to-pdf")
@Hidden
public String convertBookToPdfForm(Model model) {
model.addAttribute("currentPage", "book-to-pdf");
return "convert/book-to-pdf";
}
@ConditionalOnExpression("#{bookFormatsInstalled}")
@GetMapping("/pdf-to-book")
@Hidden
public String convertPdfToBookForm(Model model) {
model.addAttribute("currentPage", "pdf-to-book");
return "convert/pdf-to-book";
}
@GetMapping("/img-to-pdf")
@Hidden
public String convertImgToPdfForm(Model model) {

View file

@ -1,7 +1,8 @@
package stirling.software.SPDF.controller.web;
import java.io.IOException;
import java.nio.file.Files;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
@ -38,7 +39,8 @@ public class HomeWebController {
model.addAttribute("currentPage", "licenses");
Resource resource = new ClassPathResource("static/3rdPartyLicenses.json");
try {
String json = new String(Files.readAllBytes(resource.getFile().toPath()));
InputStream is = resource.getInputStream();
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
ObjectMapper mapper = new ObjectMapper();
Map<String, List<Dependency>> data =
mapper.readValue(json, new TypeReference<Map<String, List<Dependency>>>() {});

View file

@ -210,6 +210,7 @@ public class ApplicationProperties {
private String rootURIPath;
private String customStaticFilePath;
private Integer maxFileSize;
private CustomApplications customApplications;
private Boolean enableAlphaFunctionality;
@ -261,6 +262,14 @@ public class ApplicationProperties {
this.maxFileSize = maxFileSize;
}
public CustomApplications getCustomApplications() {
return customApplications != null ? customApplications : new CustomApplications();
}
public void setCustomApplications(CustomApplications customApplications) {
this.customApplications = customApplications;
}
@Override
public String toString() {
return "System [defaultLocale="
@ -273,10 +282,42 @@ public class ApplicationProperties {
+ customStaticFilePath
+ ", maxFileSize="
+ maxFileSize
+ ", customApplications="
+ customApplications
+ ", enableAlphaFunctionality="
+ enableAlphaFunctionality
+ "]";
}
public static class CustomApplications {
private boolean installBookFormats;
private boolean installAdvancedHtmlToPDF;
public boolean isInstallBookFormats() {
return installBookFormats;
}
public void setInstallBookFormats(boolean installBookFormats) {
this.installBookFormats = installBookFormats;
}
public boolean isInstallAdvancedHtmlToPDF() {
return installAdvancedHtmlToPDF;
}
public void setInstallAdvancedHtmlToPDF(boolean installAdvancedHtmlToPDF) {
this.installAdvancedHtmlToPDF = installAdvancedHtmlToPDF;
}
@Override
public String toString() {
return "CustomApplications [installBookFormats="
+ installBookFormats
+ ", installAdvancedHtmlToPDF="
+ installAdvancedHtmlToPDF
+ "]";
}
}
}
public static class Ui {

View file

@ -0,0 +1,19 @@
package stirling.software.SPDF.model.api.converters;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.api.PDFFile;
@Data
@EqualsAndHashCode(callSuper = true)
public class PdfToBookRequest extends PDFFile {
@Schema(
description = "The output Ebook format",
allowableValues = {
"epub", "mobi", "azw3", "docx", "rtf", "txt", "html", "lit", "fb2", "pdb", "lrf"
})
private String outputFormat;
}

View file

@ -14,7 +14,9 @@ import java.util.zip.ZipInputStream;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
public class FileToPdf {
public static byte[] convertHtmlToPdf(byte[] fileBytes, String fileName)
public static byte[] convertHtmlToPdf(
byte[] fileBytes, String fileName, boolean htmlFormatsInstalled)
throws IOException, InterruptedException {
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
@ -29,11 +31,22 @@ public class FileToPdf {
}
List<String> command = new ArrayList<>();
if (!htmlFormatsInstalled) {
command.add("weasyprint");
} else {
command.add("wkhtmltopdf");
command.add("--enable-local-file-access");
}
command.add(tempInputFile.toString());
command.add(tempOutputFile.toString());
ProcessExecutorResult returnCode;
if (fileName.endsWith(".zip")) {
if (htmlFormatsInstalled) {
// command.add(1, "--allow");
// command.add(2, tempInputFile.getParent().toString());
}
returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
.runCommandWithOutputHandling(
@ -97,4 +110,38 @@ public class FileToPdf {
return htmlFiles.get(0);
}
}
public static byte[] convertBookTypeToPdf(byte[] bytes, String originalFilename)
throws IOException, InterruptedException {
if (originalFilename == null || originalFilename.lastIndexOf('.') == -1) {
throw new IllegalArgumentException("Invalid original filename.");
}
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf('.'));
List<String> command = new ArrayList<>();
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
Path tempInputFile = null;
try {
// Create temp file with appropriate extension
tempInputFile = Files.createTempFile("input_", fileExtension);
Files.write(tempInputFile, bytes);
command.add("ebook-convert");
command.add(tempInputFile.toString());
command.add(tempOutputFile.toString());
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.CALIBRE)
.runCommandWithOutputHandling(command);
return Files.readAllBytes(tempOutputFile);
} finally {
// Clean up temporary files
if (tempInputFile != null) {
Files.deleteIfExists(tempInputFile);
}
Files.deleteIfExists(tempOutputFile);
}
}
}

View file

@ -4,26 +4,39 @@ import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ProcessExecutor {
private static final Logger logger = LoggerFactory.getLogger(ProcessExecutor.class);
public enum Processes {
LIBRE_OFFICE,
OCR_MY_PDF,
PYTHON_OPENCV,
GHOSTSCRIPT,
WEASYPRINT
WEASYPRINT,
INSTALL_APP,
CALIBRE
}
private static final Map<Processes, ProcessExecutor> instances = new ConcurrentHashMap<>();
public static ProcessExecutor getInstance(Processes processType) {
return getInstance(processType, true);
}
public static ProcessExecutor getInstance(Processes processType, boolean liveUpdates) {
return instances.computeIfAbsent(
processType,
key -> {
@ -34,15 +47,32 @@ public class ProcessExecutor {
case PYTHON_OPENCV -> 8;
case GHOSTSCRIPT -> 16;
case WEASYPRINT -> 16;
case INSTALL_APP -> 1;
case CALIBRE -> 1;
};
return new ProcessExecutor(semaphoreLimit);
long timeoutMinutes =
switch (key) {
case LIBRE_OFFICE -> 30;
case OCR_MY_PDF -> 30;
case PYTHON_OPENCV -> 30;
case GHOSTSCRIPT -> 5;
case WEASYPRINT -> 30;
case INSTALL_APP -> 60;
case CALIBRE -> 30;
};
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
});
}
private final Semaphore semaphore;
private final boolean liveUpdates;
private long timeoutDuration;
private ProcessExecutor(int semaphoreLimit) {
private ProcessExecutor(int semaphoreLimit, boolean liveUpdates, long timeout) {
this.semaphore = new Semaphore(semaphoreLimit);
this.liveUpdates = liveUpdates;
this.timeoutDuration = timeout;
}
public ProcessExecutorResult runCommandWithOutputHandling(List<String> command)
@ -52,12 +82,12 @@ public class ProcessExecutor {
public ProcessExecutorResult runCommandWithOutputHandling(
List<String> command, File workingDirectory) throws IOException, InterruptedException {
int exitCode = 1;
String messages = "";
int exitCode = 1;
semaphore.acquire();
try {
System.out.print("Running command: " + String.join(" ", command));
logger.info("Running command: " + String.join(" ", command));
ProcessBuilder processBuilder = new ProcessBuilder(command);
// Use the working directory if it's set
@ -81,7 +111,11 @@ public class ProcessExecutor {
String line;
while ((line = errorReader.readLine()) != null) {
errorLines.add(line);
if (liveUpdates) logger.info(line);
}
} catch (InterruptedIOException e) {
logger.warn(
"Error reader thread was interrupted due to timeout.");
} catch (IOException e) {
e.printStackTrace();
}
@ -98,7 +132,11 @@ public class ProcessExecutor {
String line;
while ((line = outputReader.readLine()) != null) {
outputLines.add(line);
if (liveUpdates) logger.info(line);
}
} catch (InterruptedIOException e) {
logger.warn(
"Error reader thread was interrupted due to timeout.");
} catch (IOException e) {
e.printStackTrace();
}
@ -108,22 +146,32 @@ public class ProcessExecutor {
outputReaderThread.start();
// Wait for the conversion process to complete
exitCode = process.waitFor();
boolean finished = process.waitFor(timeoutDuration, TimeUnit.MINUTES);
if (!finished) {
// Terminate the process
process.destroy();
// Interrupt the reader threads
errorReaderThread.interrupt();
outputReaderThread.interrupt();
throw new IOException("Process timeout exceeded.");
}
exitCode = process.exitValue();
// Wait for the reader threads to finish
errorReaderThread.join();
outputReaderThread.join();
if (!liveUpdates) {
if (outputLines.size() > 0) {
String outputMessage = String.join("\n", outputLines);
messages += outputMessage;
System.out.println("Command output:\n" + outputMessage);
logger.info("Command output:\n" + outputMessage);
}
if (errorLines.size() > 0) {
String errorMessage = String.join("\n", errorLines);
messages += errorMessage;
System.out.println("Command error output:\n" + errorMessage);
logger.warn("Command error output:\n" + errorMessage);
if (exitCode != 0) {
throw new IOException(
"Command process failed with exit code "
@ -132,6 +180,9 @@ public class ProcessExecutor {
+ errorMessage);
}
}
} else if (exitCode != 0) {
throw new IOException("Command process failed with exit code " + exitCode);
}
} finally {
semaphore.release();
}

View file

@ -9,9 +9,13 @@ security:
loginResetTimeMinutes : 120 # lock account for 2 hours after x attempts
system:
defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
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)
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)
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:
# appName: exampleAppName # Application's visible name

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-book" viewBox="0 0 16 16">
<path d="M1 2.828c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.063 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811zm7.5-.141c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783"/>
</svg>

After

Width:  |  Height:  |  Size: 770 B

View file

@ -209,8 +209,9 @@ class PdfContainer {
async exportPdf() {
const pdfDoc = await PDFLib.PDFDocument.create();
for (var i=0; i<this.pagesContainer.childNodes.length; i++) {
const img = this.pagesContainer.childNodes[i].querySelector("img");
const pageContainers = this.pagesContainer.querySelectorAll('.page-container'); // Select all .page-container elements
for (var i = 0; i < pageContainers.length; i++) {
const img = pageContainers[i].querySelector("img"); // Find the img element within each .page-container
if (!img) continue;
const pages = await pdfDoc.copyPages(img.doc, [img.pageIdx])
const page = pages[0];

View file

@ -0,0 +1,30 @@
<!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=#{BookToPDF.title}, header=#{BookToPDF.header})}"></th:block>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<h2 th:text="#{BookToPDF.header}"></h2>
<form method="post" enctype="multipart/form-data" th:action="@{api/v1/convert/book/pdf}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false)}"></div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{BookToPDF.submit}"></button>
</form>
<p class="mt-3" th:text="#{BookToPDF.help}"></p>
<p class="mt-3" th:text="#{BookToPDF.credit}"></p>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>

View file

@ -0,0 +1,55 @@
<!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=#{PDFToBook.title}, header=#{PDFToBook.header})}"></th:block>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<h2 th:text="#{PDFToBook.header}"></h2>
<form method="post" enctype="multipart/form-data"
th:action="@{api/v1/convert/pdf/book}">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="mb-3">
<label th:text="#{PDFToBook.selectText.1}"></label> <select
class="form-control" name="outputFormat">
<option value="epub">EPUB</option>
<option value="mobi">MOBI</option>
<option value="azw3">AZW3</option>
<option value="docx">DOCX</option>
<option value="rtf">RTF</option>
<option value="txt">TXT</option>
<option value="html">HTML</option>
<option value="lit">LIT</option>
<option value="fb2">FB2</option>
<option value="pdb">PDB</option>
<option value="lrf">LRF</option>
</select>
</div>
<br>
<button type="submit" id="submitBtn" class="btn btn-primary"
th:text="#{PDFToBook.submit}"></button>
</form>
<p class="mt-3" th:text="#{PDFToBook.help}"></p>
<p class="mt-3" th:text="#{PDFToBook.credit}"></p>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>

View file

@ -77,6 +77,7 @@
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('html-to-pdf', 'images/html.svg', 'home.HTMLToPDF.title', 'home.HTMLToPDF.desc', 'HTMLToPDF.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('url-to-pdf', 'images/url.svg', 'home.URLToPDF.title', 'home.URLToPDF.desc', 'URLToPDF.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('markdown-to-pdf', 'images/markdown.svg', 'home.MarkdownToPDF.title', 'home.MarkdownToPDF.desc', 'MarkdownToPDF.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('book-to-pdf', 'images/book.svg', 'home.BookToPDF.title', 'home.BookToPDF.desc', 'BookToPDF.tags')}"></div>
<hr class="dropdown-divider">
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-img', 'images/image.svg', 'home.pdfToImage.title', 'home.pdfToImage.desc', 'pdfToImage.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-word', 'images/file-earmark-word.svg', 'home.PDFToWord.title', 'home.PDFToWord.desc', 'PDFToWord.tags')}"></div>
@ -86,6 +87,7 @@
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-xml', 'images/filetype-xml.svg', 'home.PDFToXML.title', 'home.PDFToXML.desc', 'PDFToXML.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-pdfa', 'images/file-earmark-pdf.svg', 'home.pdfToPDFA.title', 'home.pdfToPDFA.desc', 'pdfToPDFA.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-csv', 'images/pdf-csv.svg', 'home.tableExtraxt.title', 'home.tableExtraxt.desc', 'pdfToPDFA.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-book', 'images/book.svg', 'home.PDFToBook.title', 'home.PDFToBook.desc', 'PDFToBook.tags')}"></div>
</div>
</li>

View file

@ -99,6 +99,8 @@
<div th:replace="~{fragments/card :: card(id='split-by-size-or-count', cardTitle=#{home.autoSizeSplitPDF.title}, cardText=#{home.autoSizeSplitPDF.desc}, cardLink='split-by-size-or-count', svgPath='images/layout-split.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='overlay-pdf', cardTitle=#{home.overlay-pdfs.title}, cardText=#{home.overlay-pdfs.desc}, cardLink='overlay-pdf', svgPath='images/overlay.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='split-pdf-by-sections', cardTitle=#{home.split-by-sections.title}, cardText=#{home.split-by-sections.desc}, cardLink='split-pdf-by-sections', svgPath='images/layout-split.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='book-to-pdf', cardTitle=#{home.BookToPDF.title}, cardText=#{home.BookToPDF.desc}, cardLink='book-to-pdf', svgPath='images/book.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='pdf-to-book', cardTitle=#{home.PDFToBook.title}, cardText=#{home.PDFToBook.desc}, cardLink='pdf-to-book', svgPath='images/book.svg')}"></div>