mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
feat(cbr-to-pdf,pdf-to-cbr): add PDF to/from CBR conversion with ebook optimization option (#4581)
# Description of Changes This pull request adds support for converting CBR (Comic Book RAR) files to PDF, optimizes CBZ/CBR-to-PDF conversion for e-readers using Ghostscript, and improves file type detection and image file handling. It introduces the `CbrUtils` and `PdfToCbrUtils` utility classes, refactors CBZ conversion logic, and integrates these features into the API controller. The most important changes are grouped below. ### CBR Support and Conversion: - Added the `com.github.junrar:junrar` dependency to support RAR/CBR archive extraction in `build.gradle`. (https://github.com/junrar/junrar and https://github.com/junrar/junrar?tab=License-1-ov-file#readme for repo and license) - Introduced the new utility class `CbrUtils` for converting CBR files to PDF, including image extraction, sorting, and error handling. - Added the `PdfToCbrUtils` utility class to convert PDF files into CBR archives by rendering each page as an image and packaging them. ### CBZ/CBR Conversion Optimization: - Refactored `CbzUtils.convertCbzToPdf` to support optional Ghostscript optimization for e-reader compatibility and added a new method for this. - Added `GeneralUtils.optimizePdfWithGhostscript`, which uses Ghostscript to optimize PDFs for e-readers, and integrated error handling. ### API Controller Integration: - Updated `ConvertImgPDFController` to support CBR conversion, CBZ/CBR optimization toggling, and Ghostscript availability checks. ### Endpoints <img width="1298" height="522" alt="image" src="https://github.com/user-attachments/assets/144d3e03-a637-451a-9c35-f784b2a66dc1" /> <img width="1279" height="472" alt="image" src="https://github.com/user-attachments/assets/879f221d-b775-4224-8edb-a23dbea6a0ca" /> ### UI <img width="384" height="105" alt="image" src="https://github.com/user-attachments/assets/5f861943-0706-4fad-8775-c40a9c1f3170" /> ### File Type and Image Detection Improvements: - Improved file extension detection for comic book files and image files in `CbzUtils` and added a shared regex pattern utility for image files. ### Additional notes: - Please keep in mind new the dependency, this is not dependency-free implementation (as opposed to CBZ converter) - RAR 5 currently not supported. (because JUNRAR does not support it) - Added the new ebook optimization func to GeneralUtils since we'll soon (hopefully) at least 3 book/ebook formats (EPUB, CBZ, CBR) all of which can use it. - Once again this has been thoroughly tested but can't share actual "real life" file due to copyright. Closes: #775 <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [x] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [x] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [x] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Signed-off-by: Balázs Szücs <[email protected]>
This commit is contained in:
+103
-9
@@ -8,6 +8,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipEntry;
|
||||
@@ -32,15 +33,20 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertCbrToPdfRequest;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertCbzToPdfRequest;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertPdfToCbrRequest;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertPdfToCbzRequest;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.CbrUtils;
|
||||
import stirling.software.common.util.CbzUtils;
|
||||
import stirling.software.common.util.CheckProgramInstall;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.PdfToCbrUtils;
|
||||
import stirling.software.common.util.PdfToCbzUtils;
|
||||
import stirling.software.common.util.PdfUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
@@ -58,10 +64,15 @@ public class ConvertImgPDFController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
private final EndpointConfiguration endpointConfiguration;
|
||||
private static final Pattern EXTENSION_PATTERN =
|
||||
RegexPatternUtils.getInstance().getPattern(RegexPatternUtils.getExtensionRegex());
|
||||
private static final String DEFAULT_COMIC_NAME = "comic";
|
||||
|
||||
private boolean isGhostscriptEnabled() {
|
||||
return endpointConfiguration.isGroupEnabled("Ghostscript");
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/img")
|
||||
@Operation(
|
||||
summary = "Convert PDF to image(s)",
|
||||
@@ -257,17 +268,29 @@ public class ConvertImgPDFController {
|
||||
description =
|
||||
"This endpoint converts a CBZ (ZIP) comic book archive to a PDF file. "
|
||||
+ "Input:CBZ Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> convertCbzToPdf(@ModelAttribute ConvertCbzToPdfRequest request)
|
||||
public ResponseEntity<?> convertCbzToPdf(@ModelAttribute ConvertCbzToPdfRequest request)
|
||||
throws IOException {
|
||||
MultipartFile file = request.getFileInput();
|
||||
boolean optimizeForEbook = request.isOptimizeForEbook();
|
||||
|
||||
// Disable optimization if Ghostscript is not available
|
||||
if (optimizeForEbook && !isGhostscriptEnabled()) {
|
||||
log.warn("Ghostscript optimization requested but Ghostscript is not enabled/available");
|
||||
optimizeForEbook = false;
|
||||
}
|
||||
|
||||
byte[] pdfBytes;
|
||||
try {
|
||||
pdfBytes = CbzUtils.convertCbzToPdf(file, pdfDocumentFactory, tempFileManager);
|
||||
pdfBytes =
|
||||
CbzUtils.convertCbzToPdf(
|
||||
file, pdfDocumentFactory, tempFileManager, optimizeForEbook);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
String message = ex.getMessage() == null ? "Invalid CBZ file" : ex.getMessage();
|
||||
Map<String, Object> errorBody =
|
||||
Map.of("error", "Invalid CBZ file", "message", message, "trace", "");
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.contentType(MediaType.TEXT_PLAIN)
|
||||
.body(message.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(errorBody);
|
||||
}
|
||||
|
||||
String filename = createConvertedFilename(file.getOriginalFilename(), "_converted.pdf");
|
||||
@@ -281,12 +304,12 @@ public class ConvertImgPDFController {
|
||||
description =
|
||||
"This endpoint converts a PDF file to a CBZ (ZIP) comic book archive. "
|
||||
+ "Input:PDF Output:CBZ Type:SISO")
|
||||
public ResponseEntity<byte[]> convertPdfToCbz(@ModelAttribute ConvertPdfToCbzRequest request)
|
||||
public ResponseEntity<?> convertPdfToCbz(@ModelAttribute ConvertPdfToCbzRequest request)
|
||||
throws IOException {
|
||||
MultipartFile file = request.getFileInput();
|
||||
Integer dpi = request.getDpi();
|
||||
int dpi = request.getDpi();
|
||||
|
||||
if (dpi == null || dpi <= 0) {
|
||||
if (dpi <= 0) {
|
||||
dpi = 300;
|
||||
}
|
||||
|
||||
@@ -295,9 +318,11 @@ public class ConvertImgPDFController {
|
||||
cbzBytes = PdfToCbzUtils.convertPdfToCbz(file, dpi, pdfDocumentFactory);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
String message = ex.getMessage() == null ? "Invalid PDF file" : ex.getMessage();
|
||||
Map<String, Object> errorBody =
|
||||
Map.of("error", "Invalid PDF file", "message", message, "trace", "");
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.contentType(MediaType.TEXT_PLAIN)
|
||||
.body(message.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(errorBody);
|
||||
}
|
||||
|
||||
String filename = createConvertedFilename(file.getOriginalFilename(), "_converted.cbz");
|
||||
@@ -306,6 +331,75 @@ public class ConvertImgPDFController {
|
||||
cbzBytes, filename, MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/cbr/pdf")
|
||||
@Operation(
|
||||
summary = "Convert CBR comic book archive to PDF",
|
||||
description =
|
||||
"This endpoint converts a CBR (RAR) comic book archive to a PDF file. "
|
||||
+ "Input:CBR Output:PDF Type:SISO")
|
||||
public ResponseEntity<?> convertCbrToPdf(@ModelAttribute ConvertCbrToPdfRequest request)
|
||||
throws IOException {
|
||||
MultipartFile file = request.getFileInput();
|
||||
boolean optimizeForEbook = request.isOptimizeForEbook();
|
||||
|
||||
// Disable optimization if Ghostscript is not available
|
||||
if (optimizeForEbook && !isGhostscriptEnabled()) {
|
||||
log.warn("Ghostscript optimization requested but Ghostscript is not enabled/available");
|
||||
optimizeForEbook = false;
|
||||
}
|
||||
|
||||
byte[] pdfBytes;
|
||||
try {
|
||||
pdfBytes =
|
||||
CbrUtils.convertCbrToPdf(
|
||||
file, pdfDocumentFactory, tempFileManager, optimizeForEbook);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
String message = ex.getMessage() == null ? "Invalid CBR file" : ex.getMessage();
|
||||
Map<String, Object> errorBody =
|
||||
Map.of("error", "Invalid CBR file", "message", message, "trace", "");
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(errorBody);
|
||||
}
|
||||
|
||||
String filename = createConvertedFilename(file.getOriginalFilename(), "_converted.pdf");
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, filename);
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/cbr")
|
||||
@Operation(
|
||||
summary = "Convert PDF to CBR comic book archive",
|
||||
description =
|
||||
"This endpoint converts a PDF file to a CBR-like (ZIP-based) comic book archive. "
|
||||
+ "Note: Output is ZIP-based for compatibility. Input:PDF Output:CBR Type:SISO")
|
||||
public ResponseEntity<?> convertPdfToCbr(@ModelAttribute ConvertPdfToCbrRequest request)
|
||||
throws IOException {
|
||||
MultipartFile file = request.getFileInput();
|
||||
int dpi = request.getDpi();
|
||||
|
||||
if (dpi <= 0) {
|
||||
dpi = 300;
|
||||
}
|
||||
|
||||
byte[] cbrBytes;
|
||||
try {
|
||||
cbrBytes = PdfToCbrUtils.convertPdfToCbr(file, dpi, pdfDocumentFactory);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
String message = ex.getMessage() == null ? "Invalid PDF file" : ex.getMessage();
|
||||
Map<String, Object> errorBody =
|
||||
Map.of("error", "Invalid PDF file", "message", message, "trace", "");
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(errorBody);
|
||||
}
|
||||
|
||||
String filename = createConvertedFilename(file.getOriginalFilename(), "_converted.cbr");
|
||||
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
cbrBytes, filename, MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
|
||||
private String createConvertedFilename(String originalFilename, String suffix) {
|
||||
if (originalFilename == null) {
|
||||
return GeneralUtils.generateFilename(DEFAULT_COMIC_NAME, suffix);
|
||||
|
||||
+14
@@ -37,6 +37,20 @@ public class ConverterWebController {
|
||||
return "convert/pdf-to-cbz";
|
||||
}
|
||||
|
||||
@GetMapping("/cbr-to-pdf")
|
||||
@Hidden
|
||||
public String convertCbrToPdfForm(Model model) {
|
||||
model.addAttribute("currentPage", "cbr-to-pdf");
|
||||
return "convert/cbr-to-pdf";
|
||||
}
|
||||
|
||||
@GetMapping("/pdf-to-cbr")
|
||||
@Hidden
|
||||
public String convertPdfToCbrForm(Model model) {
|
||||
model.addAttribute("currentPage", "pdf-to-cbr");
|
||||
return "convert/pdf-to-cbr";
|
||||
}
|
||||
|
||||
@GetMapping("/html-to-pdf")
|
||||
@Hidden
|
||||
public String convertHTMLToPdfForm(Model model) {
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package stirling.software.SPDF.model.api.converters;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
public class ConvertCbrToPdfRequest {
|
||||
|
||||
@Schema(
|
||||
description = "The input CBR file to be converted to a PDF file",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private MultipartFile fileInput;
|
||||
|
||||
@Schema(
|
||||
description = "Optimize the output PDF for ebook reading using Ghostscript",
|
||||
defaultValue = "false")
|
||||
private boolean optimizeForEbook;
|
||||
}
|
||||
+5
@@ -15,4 +15,9 @@ public class ConvertCbzToPdfRequest {
|
||||
description = "The input CBZ file to be converted to a PDF file",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private MultipartFile fileInput;
|
||||
|
||||
@Schema(
|
||||
description = "Optimize the output PDF for ebook reading using Ghostscript",
|
||||
defaultValue = "false")
|
||||
private boolean optimizeForEbook;
|
||||
}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package stirling.software.SPDF.model.api.converters;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
public class ConvertPdfToCbrRequest {
|
||||
|
||||
@Schema(
|
||||
description = "The input PDF file to be converted to a CBR file",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private MultipartFile fileInput;
|
||||
|
||||
@Schema(
|
||||
description = "The DPI (Dots Per Inch) for rendering PDF pages as images",
|
||||
example = "150",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private int dpi = 150;
|
||||
}
|
||||
+3
-3
@@ -18,7 +18,7 @@ public class ConvertPdfToCbzRequest {
|
||||
|
||||
@Schema(
|
||||
description = "The DPI (Dots Per Inch) for rendering PDF pages as images",
|
||||
example = "300",
|
||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||
private Integer dpi = 300;
|
||||
example = "150",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private int dpi = 150;
|
||||
}
|
||||
|
||||
@@ -610,10 +610,18 @@ home.cbzToPdf.title=CBZ to PDF
|
||||
home.cbzToPdf.desc=Convert CBZ comic book archives to PDF format.
|
||||
cbzToPdf.tags=conversion,comic,book,archive,cbz,zip
|
||||
|
||||
home.cbrToPdf.title=CBR to PDF
|
||||
home.cbrToPdf.desc=Convert CBR comic book archives to PDF format.
|
||||
cbrToPdf.tags=conversion,comic,book,archive,cbr,rar
|
||||
|
||||
home.pdfToCbz.title=PDF to CBZ
|
||||
home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives.
|
||||
pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf
|
||||
|
||||
home.pdfToCbr.title=PDF to CBR
|
||||
home.pdfToCbr.desc=Convert PDF files to CBR comic book archives.
|
||||
pdfToCbr.tags=conversion,comic,book,archive,cbr,rar
|
||||
|
||||
home.pdfToImage.title=PDF to Image
|
||||
home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF, PSD)
|
||||
pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop
|
||||
@@ -1445,6 +1453,7 @@ cbzToPDF.title=CBZ to PDF
|
||||
cbzToPDF.header=CBZ to PDF
|
||||
cbzToPDF.submit=Convert to PDF
|
||||
cbzToPDF.selectText=Select CBZ file
|
||||
cbzToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript)
|
||||
|
||||
#pdfToCBZ
|
||||
pdfToCBZ.title=PDF to CBZ
|
||||
@@ -1453,6 +1462,21 @@ pdfToCBZ.submit=Convert to CBZ
|
||||
pdfToCBZ.selectText=Select PDF file
|
||||
pdfToCBZ.dpi=DPI (Dots Per Inch)
|
||||
|
||||
#cbrToPDF
|
||||
cbrToPDF.title=CBR to PDF
|
||||
cbrToPDF.header=CBR to PDF
|
||||
cbrToPDF.submit=Convert to PDF
|
||||
cbrToPDF.selectText=Select CBR file
|
||||
cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript)
|
||||
|
||||
#pdfToCBR
|
||||
pdfToCBR.title=PDF to CBR
|
||||
pdfToCBR.header=PDF to CBR
|
||||
pdfToCBR.submit=Convert to CBR
|
||||
pdfToCBR.selectText=Select PDF file
|
||||
pdfToCBR.dpi=DPI (Dots Per Inch)
|
||||
pdfToCBR.dpiHelp=Higher DPI results in better quality but larger file size.
|
||||
|
||||
#pdfToImage
|
||||
pdfToImage.title=PDF to Image
|
||||
pdfToImage.header=PDF to Image
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:data-language="${#locale.toString()}" th:dir="#{language.direction}" th:lang="${#locale.language}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{cbrToPDF.title}, header=#{cbrToPDF.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon convertto">auto_stories</span>
|
||||
<span class="tool-header-text" th:text="#{cbrToPDF.header}"></span>
|
||||
</div>
|
||||
<form enctype="multipart/form-data" id="cbrToPDFForm" method="post"
|
||||
th:action="@{'/api/v1/convert/cbr/pdf'}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.cbr,.rar', inputText=#{cbrToPDF.selectText})}">
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3" th:if="${@endpointConfiguration.isGroupEnabled('Ghostscript')}">
|
||||
<input id="optimizeForEbook" name="optimizeForEbook" type="checkbox" value="true">
|
||||
<label for="optimizeForEbook" th:text="#{cbrToPDF.optimizeForEbook}">Optimize PDF for ebook readers (uses Ghostscript)</label>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button class="btn btn-primary" id="submitBtn" th:text="#{cbrToPDF.submit}" type="submit">Convert to PDF</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -25,6 +25,11 @@
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.cbz,.zip', inputText=#{cbzToPDF.selectText})}">
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3" th:if="${@endpointConfiguration.isGroupEnabled('Ghostscript')}">
|
||||
<input id="optimizeForEbook" name="optimizeForEbook" type="checkbox" value="true">
|
||||
<label for="optimizeForEbook" th:text="#{cbzToPDF.optimizeForEbook}">Optimize PDF for ebook readers (uses Ghostscript)</label>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button class="btn btn-primary" id="submitBtn" th:text="#{cbzToPDF.submit}" type="submit">Convert to PDF</button>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:data-language="${#locale.toString()}" th:dir="#{language.direction}" th:lang="${#locale.language}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{pdfToCBR.title}, header=#{pdfToCBR.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon convertto">auto_stories</span>
|
||||
<span class="tool-header-text" th:text="#{pdfToCBR.header}"></span>
|
||||
</div>
|
||||
<form enctype="multipart/form-data" id="pdfToCBRForm" method="post"
|
||||
th:action="@{'/api/v1/convert/pdf/cbr'}">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.pdf', inputText=#{pdfToCBR.selectText})}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="dpi" th:text="#{pdfToCBR.dpi}">DPI:</label>
|
||||
<input class="form-control" id="dpi" max="600" min="50" name="dpi" type="number" value="300">
|
||||
<div class="form-text" th:text="#{pdfToCBR.dpiHelp}">Higher DPI results in better quality but larger file size.</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button class="btn btn-primary" id="submitBtn" th:text="#{pdfToCBR.submit}" type="submit">Convert to CBR</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -50,6 +50,9 @@
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('cbz-to-pdf', 'auto_stories', 'home.cbzToPdf.title', 'home.cbzToPdf.desc', 'cbzToPdf.tags', 'convertto')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('cbr-to-pdf', 'auto_stories', 'home.cbrToPdf.title', 'home.cbrToPdf.desc', 'cbrToPdf.tags', 'convertto')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('file-to-pdf', 'draft', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags', 'convertto')}">
|
||||
</div>
|
||||
@@ -77,6 +80,9 @@
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-cbz', 'auto_stories', 'home.pdfToCbz.title', 'home.pdfToCbz.desc', 'pdfToCbz.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-cbr', 'auto_stories', 'home.pdfToCbr.title', 'home.pdfToCbr.desc', 'pdfToCbr.tags', 'convert')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-pdfa', 'picture_as_pdf', 'home.pdfToPDFA.title', 'home.pdfToPDFA.desc', 'pdfToPDFA.tags', 'convert')}">
|
||||
</div>
|
||||
@@ -114,6 +120,9 @@
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('cbz-to-pdf', 'auto_stories', 'home.cbzToPdf.title', 'home.cbzToPdf.desc', 'cbzToPdf.tags', 'convertto')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('cbr-to-pdf', 'auto_stories', 'home.cbrToPdf.title', 'home.cbrToPdf.desc', 'cbrToPdf.tags', 'convertto')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/navbarEntry :: navbarEntry('file-to-pdf', 'draft', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags', 'convertto')}">
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user