mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-01 01:21:18 +01:00
feat(pdf-to-cbr): integrate RAR for CBR output generation (#4626)
# Description of Changes This pull request introduces full support for generating true CBR (Comic Book RAR) archives from PDF files using the local RAR CLI ### CBR Conversion Implementation: - Refactored `PdfToCbrUtils.java` to generate image files for each PDF page, invoke the RAR CLI to create a `.cbr` archive, and clean up temporary files after conversion.. ### Dependency & Endpoint Management: - Added RAR as a required external dependency in `ExternalAppDepConfig.java` and checks for its availability, disabling related endpoints if missing. - Registered new endpoints under the "RAR" group in `EndpointConfiguration.java` and updated group validation logic. ### Controller and API Updates: - Updated the API controller to clarify that the output is a true CBR archive created with RAR, not ZIP-based. - Modified the web controller to check for endpoint availability and return a 404 error if the CBR conversion feature is disabled. ### Sample logs/verification: Conversion command > 23:12:41.552 [qtp1634254747-43] INFO s.s.common.util.ProcessExecutor - Running command: rar a -m5 -ep1 output.cbr page_001.png > 23:12:41.571 [Thread-25] INFO s.s.common.util.ProcessExecutor - > 23:12:41.571 [Thread-25] INFO s.s.common.util.ProcessExecutor - RAR 7.12 Copyright (c) 1993-2025 Alexander Roshal 23 Jun 2025 > 23:12:41.571 [Thread-25] INFO s.s.common.util.ProcessExecutor - Trial version Type 'rar -?' for help > 23:12:41.571 [Thread-25] INFO s.s.common.util.ProcessExecutor - > 23:12:41.571 [Thread-25] INFO s.s.common.util.ProcessExecutor - Evaluation copy. Please register. > 23:12:41.571 [Thread-25] INFO s.s.common.util.ProcessExecutor - > 23:12:41.572 [Thread-25] INFO s.s.common.util.ProcessExecutor - Creating archive output.cbr > 23:12:41.578 [Thread-25] INFO s.s.common.util.ProcessExecutor - > 23:12:41.587 [Thread-25] INFO s.s.common.util.ProcessExecutor - Adding page_001.png OK > 23:12:41.587 [Thread-25] INFO s.s.common.util.ProcessExecutor - Done Verification whether its RAR (not included in the code; was to verify whether the code works) > ~/Downloads > ❯ unrar l lorem-ipsum_converted.cbr > > UNRAR 7.12 freeware Copyright (c) 1993-2025 Alexander Roshal > > Archive: lorem-ipsum_converted.cbr > Details: RAR 5 > > Attributes Size Date Time Name > ----------- --------- ---------- ----- ---- > -rw-r--r-- 105955 2025-10-07 23:12 page_001.png > ----------- --------- ---------- ----- ---- > 105955 1 Logs on startup with no RAR CLI > INFO:unoserver:Started. > 12:09:16.592 [main] INFO s.s.p.s.configuration.DatabaseConfig - Using default H2 database > INFO:unoserver:Server PID: 46 > 12:09:21.281 [main] INFO s.s.c.config.TempFileConfiguration - Created temporary directory: /tmp/stirling-pdf/stirling-pdf > 12:09:21.329 [main] WARN s.s.SPDF.config.ExternalAppDepConfig - Missing dependency: rar - Disabling group: RAR (Affected features: Pdf/cbr, PDF To Cbr) > 12:09:22.066 [main] INFO s.s.S.config.EndpointConfiguration - Disabled tool groups: RAR (endpoints may have alternative implementations) > 12:09:22.066 [main] INFO s.s.S.config.EndpointConfiguration - Disabled functional groups: enterprise > 12:09:22.066 [main] INFO s.s.S.config.EndpointConfiguration - Total disabled endpoints: 3. Disabled endpoints: pdf-to-cbr, pdf/cbr, url-to-pdf > 12:09:22.407 [main] INFO s.s.p.s.service.DatabaseService - Source directory does not exist: configs/db/backup > 12:09:23.092 [main] INFO s.software.common.util.FileMonitor - Monitoring directory: ./pipeline/watchedFolders > 12:09:23.721 [main] INFO s.s.c.service.TempFileCleanupService - Created LibreOffice temp directory: /tmp/stirling-pdf/stirling-pdf/libreoffice <!-- 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 - [ ] 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) - [ ] 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 <bszucs1209@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
0a02e3e231
commit
599beb7912
@ -2,9 +2,13 @@ package stirling.software.common.util;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
@ -17,6 +21,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
|
||||
@Slf4j
|
||||
public class PdfToCbrUtils {
|
||||
@ -55,9 +60,9 @@ public class PdfToCbrUtils {
|
||||
private static byte[] createCbrFromPdf(PDDocument document, int dpi) throws IOException {
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
|
||||
try (ByteArrayOutputStream cbrOutputStream = new ByteArrayOutputStream();
|
||||
ZipOutputStream zipOut = new ZipOutputStream(cbrOutputStream)) {
|
||||
|
||||
Path tempDir = Files.createTempDirectory("stirling-pdf-cbr-");
|
||||
List<Path> generatedImages = new ArrayList<>();
|
||||
try {
|
||||
int totalPages = document.getNumberOfPages();
|
||||
|
||||
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||
@ -66,12 +71,10 @@ public class PdfToCbrUtils {
|
||||
pdfRenderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB);
|
||||
|
||||
String imageFilename = String.format("page_%03d.png", pageIndex + 1);
|
||||
Path imagePath = tempDir.resolve(imageFilename);
|
||||
|
||||
ZipEntry zipEntry = new ZipEntry(imageFilename);
|
||||
zipOut.putNextEntry(zipEntry);
|
||||
|
||||
ImageIO.write(image, "PNG", zipOut);
|
||||
zipOut.closeEntry();
|
||||
ImageIO.write(image, "PNG", imagePath.toFile());
|
||||
generatedImages.add(imagePath);
|
||||
|
||||
} catch (IOException e) {
|
||||
log.warn("Error processing page {}: {}", pageIndex + 1, e.getMessage());
|
||||
@ -82,8 +85,79 @@ public class PdfToCbrUtils {
|
||||
}
|
||||
}
|
||||
|
||||
zipOut.finish();
|
||||
return cbrOutputStream.toByteArray();
|
||||
if (generatedImages.isEmpty()) {
|
||||
throw new IOException("Failed to render any pages to images for CBR conversion");
|
||||
}
|
||||
|
||||
return createRarArchive(tempDir, generatedImages);
|
||||
} finally {
|
||||
cleanupTempFiles(generatedImages, tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] createRarArchive(Path tempDir, List<Path> images) throws IOException {
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("rar");
|
||||
command.add("a");
|
||||
command.add("-m5");
|
||||
command.add("-ep1");
|
||||
|
||||
Path rarFile = tempDir.resolve("output.cbr");
|
||||
command.add(rarFile.getFileName().toString());
|
||||
|
||||
for (Path image : images) {
|
||||
command.add(image.getFileName().toString());
|
||||
}
|
||||
|
||||
ProcessExecutor executor =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.INSTALL_APP);
|
||||
try {
|
||||
ProcessExecutorResult result =
|
||||
executor.runCommandWithOutputHandling(command, tempDir.toFile());
|
||||
if (result.getRc() != 0) {
|
||||
throw new IOException("RAR command failed: " + result.getMessages());
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("RAR command interrupted", e);
|
||||
}
|
||||
|
||||
if (!Files.exists(rarFile)) {
|
||||
throw new IOException("RAR file was not created");
|
||||
}
|
||||
|
||||
try (FileInputStream fis = new FileInputStream(rarFile.toFile());
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
fis.transferTo(baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static void cleanupTempFiles(List<Path> images, Path tempDir) {
|
||||
for (Path image : images) {
|
||||
try {
|
||||
Files.deleteIfExists(image);
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to delete temp image file {}: {}", image, e.getMessage());
|
||||
}
|
||||
}
|
||||
if (tempDir != null) {
|
||||
try (var paths = Files.walk(tempDir)) {
|
||||
paths.sorted(Comparator.reverseOrder())
|
||||
.forEach(
|
||||
path -> {
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
} catch (IOException e) {
|
||||
log.warn(
|
||||
"Failed to delete temp path {}: {}",
|
||||
path,
|
||||
e.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to clean up temp directory {}: {}", tempDir, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -386,6 +386,7 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Java", "pdf-to-markdown");
|
||||
addEndpointToGroup("Java", "add-attachments");
|
||||
addEndpointToGroup("Java", "compress-pdf");
|
||||
addEndpointToGroup("rar", "pdf-to-cbr");
|
||||
|
||||
// Javascript
|
||||
addEndpointToGroup("Javascript", "pdf-organizer");
|
||||
@ -484,7 +485,8 @@ public class EndpointConfiguration {
|
||||
|| "Java".equals(group)
|
||||
|| "Javascript".equals(group)
|
||||
|| "Weasyprint".equals(group)
|
||||
|| "Pdftohtml".equals(group);
|
||||
|| "Pdftohtml".equals(group)
|
||||
|| "rar".equals(group);
|
||||
}
|
||||
|
||||
private boolean isEndpointEnabledDirectly(String endpoint) {
|
||||
|
@ -43,6 +43,7 @@ public class ExternalAppDepConfig {
|
||||
put(unoconvPath, List.of("Unoconvert"));
|
||||
put("qpdf", List.of("qpdf"));
|
||||
put("tesseract", List.of("tesseract"));
|
||||
put("rar", List.of("rar")); // Required for real CBR output
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -120,6 +121,7 @@ public class ExternalAppDepConfig {
|
||||
checkDependencyAndDisableGroup(weasyprintPath);
|
||||
checkDependencyAndDisableGroup("pdftohtml");
|
||||
checkDependencyAndDisableGroup(unoconvPath);
|
||||
checkDependencyAndDisableGroup("rar");
|
||||
// Special handling for Python/OpenCV dependencies
|
||||
boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python");
|
||||
if (!pythonAvailable) {
|
||||
|
@ -371,8 +371,8 @@ public class ConvertImgPDFController {
|
||||
@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")
|
||||
"This endpoint converts a PDF file to a CBR comic book archive using the local RAR CLI. "
|
||||
+ "Input:PDF Output:CBR Type:SISO")
|
||||
public ResponseEntity<?> convertPdfToCbr(@ModelAttribute ConvertPdfToCbrRequest request)
|
||||
throws IOException {
|
||||
MultipartFile file = request.getFileInput();
|
||||
|
@ -1,13 +1,16 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.ApplicationContextProvider;
|
||||
import stirling.software.common.util.CheckProgramInstall;
|
||||
@ -47,6 +50,10 @@ public class ConverterWebController {
|
||||
@GetMapping("/pdf-to-cbr")
|
||||
@Hidden
|
||||
public String convertPdfToCbrForm(Model model) {
|
||||
if (!ApplicationContextProvider.getBean(EndpointConfiguration.class)
|
||||
.isEndpointEnabled("pdf-to-cbr")) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
model.addAttribute("currentPage", "pdf-to-cbr");
|
||||
return "convert/pdf-to-cbr";
|
||||
}
|
||||
|
@ -62,7 +62,10 @@ public class InitialSecuritySetup {
|
||||
|
||||
boolean jwtEnabled = jwtProperties.isEnabled();
|
||||
if (!v2Enabled || !jwtEnabled) {
|
||||
log.debug("V2 enabled: {}, JWT enabled: {} - disabling all JWT features", v2Enabled, jwtEnabled);
|
||||
log.debug(
|
||||
"V2 enabled: {}, JWT enabled: {} - disabling all JWT features",
|
||||
v2Enabled,
|
||||
jwtEnabled);
|
||||
|
||||
jwtProperties.setKeyCleanup(false);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user