commit
50ee829e5f
23 changed files with 652 additions and 48 deletions
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -115,4 +115,4 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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("[.][^.]+$", "")
|
||||
|
|
|
@ -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("[.][^.]+$", "")
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<>();
|
||||
command.add("weasyprint");
|
||||
if (!htmlFormatsInstalled) {
|
||||
command.add("weasyprint");
|
||||
} else {
|
||||
command.add("wkhtmltopdf");
|
||||
}
|
||||
command.add(URL);
|
||||
command.add(tempOutputFile.toString());
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>>>() {});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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<>();
|
||||
command.add("weasyprint");
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,29 +146,42 @@ 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 (outputLines.size() > 0) {
|
||||
String outputMessage = String.join("\n", outputLines);
|
||||
messages += outputMessage;
|
||||
System.out.println("Command output:\n" + outputMessage);
|
||||
}
|
||||
|
||||
if (errorLines.size() > 0) {
|
||||
String errorMessage = String.join("\n", errorLines);
|
||||
messages += errorMessage;
|
||||
System.out.println("Command error output:\n" + errorMessage);
|
||||
if (exitCode != 0) {
|
||||
throw new IOException(
|
||||
"Command process failed with exit code "
|
||||
+ exitCode
|
||||
+ ". Error message: "
|
||||
+ errorMessage);
|
||||
if (!liveUpdates) {
|
||||
if (outputLines.size() > 0) {
|
||||
String outputMessage = String.join("\n", outputLines);
|
||||
messages += outputMessage;
|
||||
logger.info("Command output:\n" + outputMessage);
|
||||
}
|
||||
|
||||
if (errorLines.size() > 0) {
|
||||
String errorMessage = String.join("\n", errorLines);
|
||||
messages += errorMessage;
|
||||
logger.warn("Command error output:\n" + errorMessage);
|
||||
if (exitCode != 0) {
|
||||
throw new IOException(
|
||||
"Command process failed with exit code "
|
||||
+ exitCode
|
||||
+ ". Error message: "
|
||||
+ errorMessage);
|
||||
}
|
||||
}
|
||||
} else if (exitCode != 0) {
|
||||
throw new IOException("Command process failed with exit code " + exitCode);
|
||||
}
|
||||
} finally {
|
||||
semaphore.release();
|
||||
|
|
|
@ -9,10 +9,14 @@ 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
|
||||
# homeDescription: I am a description # Short description or tagline shown on homepage.
|
||||
|
|
3
src/main/resources/static/images/book.svg
Normal file
3
src/main/resources/static/images/book.svg
Normal 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 |
|
@ -209,20 +209,21 @@ 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");
|
||||
if (!img) continue;
|
||||
const pages = await pdfDoc.copyPages(img.doc, [img.pageIdx])
|
||||
const page = pages[0];
|
||||
|
||||
const rotation = img.style.rotate;
|
||||
if (rotation) {
|
||||
const rotationAngle = parseInt(rotation.replace(/[^\d-]/g, ''));
|
||||
page.setRotation(PDFLib.degrees(page.getRotation().angle + rotationAngle))
|
||||
}
|
||||
|
||||
pdfDoc.addPage(page);
|
||||
}
|
||||
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];
|
||||
|
||||
const rotation = img.style.rotate;
|
||||
if (rotation) {
|
||||
const rotationAngle = parseInt(rotation.replace(/[^\d-]/g, ''));
|
||||
page.setRotation(PDFLib.degrees(page.getRotation().angle + rotationAngle))
|
||||
}
|
||||
|
||||
pdfDoc.addPage(page);
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
const pdfBlob = new Blob([pdfBytes], { type: 'application/pdf' });
|
||||
const url = URL.createObjectURL(pdfBlob);
|
||||
|
|
30
src/main/resources/templates/convert/book-to-pdf.html
Normal file
30
src/main/resources/templates/convert/book-to-pdf.html
Normal 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>
|
55
src/main/resources/templates/convert/pdf-to-book.html
Normal file
55
src/main/resources/templates/convert/pdf-to-book.html
Normal 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>
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue