Skip to content

Commit 599beb7

Browse files
feat(pdf-to-cbr): integrate RAR for CBR output generation (Stirling-Tools#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 <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 0a02e3e commit 599beb7

File tree

6 files changed

+104
-16
lines changed

6 files changed

+104
-16
lines changed

app/common/src/main/java/stirling/software/common/util/PdfToCbrUtils.java

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
import java.awt.image.BufferedImage;
44
import java.io.ByteArrayOutputStream;
5+
import java.io.FileInputStream;
56
import java.io.IOException;
6-
import java.util.zip.ZipEntry;
7-
import java.util.zip.ZipOutputStream;
7+
import java.nio.file.Files;
8+
import java.nio.file.Path;
9+
import java.util.ArrayList;
10+
import java.util.Comparator;
11+
import java.util.List;
812

913
import javax.imageio.ImageIO;
1014

@@ -17,6 +21,7 @@
1721
import lombok.extern.slf4j.Slf4j;
1822

1923
import stirling.software.common.service.CustomPDFDocumentFactory;
24+
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
2025

2126
@Slf4j
2227
public class PdfToCbrUtils {
@@ -55,9 +60,9 @@ private static void validatePdfFile(MultipartFile file) {
5560
private static byte[] createCbrFromPdf(PDDocument document, int dpi) throws IOException {
5661
PDFRenderer pdfRenderer = new PDFRenderer(document);
5762

58-
try (ByteArrayOutputStream cbrOutputStream = new ByteArrayOutputStream();
59-
ZipOutputStream zipOut = new ZipOutputStream(cbrOutputStream)) {
60-
63+
Path tempDir = Files.createTempDirectory("stirling-pdf-cbr-");
64+
List<Path> generatedImages = new ArrayList<>();
65+
try {
6166
int totalPages = document.getNumberOfPages();
6267

6368
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
@@ -66,12 +71,10 @@ private static byte[] createCbrFromPdf(PDDocument document, int dpi) throws IOEx
6671
pdfRenderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB);
6772

6873
String imageFilename = String.format("page_%03d.png", pageIndex + 1);
74+
Path imagePath = tempDir.resolve(imageFilename);
6975

70-
ZipEntry zipEntry = new ZipEntry(imageFilename);
71-
zipOut.putNextEntry(zipEntry);
72-
73-
ImageIO.write(image, "PNG", zipOut);
74-
zipOut.closeEntry();
76+
ImageIO.write(image, "PNG", imagePath.toFile());
77+
generatedImages.add(imagePath);
7578

7679
} catch (IOException e) {
7780
log.warn("Error processing page {}: {}", pageIndex + 1, e.getMessage());
@@ -82,8 +85,79 @@ private static byte[] createCbrFromPdf(PDDocument document, int dpi) throws IOEx
8285
}
8386
}
8487

85-
zipOut.finish();
86-
return cbrOutputStream.toByteArray();
88+
if (generatedImages.isEmpty()) {
89+
throw new IOException("Failed to render any pages to images for CBR conversion");
90+
}
91+
92+
return createRarArchive(tempDir, generatedImages);
93+
} finally {
94+
cleanupTempFiles(generatedImages, tempDir);
95+
}
96+
}
97+
98+
private static byte[] createRarArchive(Path tempDir, List<Path> images) throws IOException {
99+
List<String> command = new ArrayList<>();
100+
command.add("rar");
101+
command.add("a");
102+
command.add("-m5");
103+
command.add("-ep1");
104+
105+
Path rarFile = tempDir.resolve("output.cbr");
106+
command.add(rarFile.getFileName().toString());
107+
108+
for (Path image : images) {
109+
command.add(image.getFileName().toString());
110+
}
111+
112+
ProcessExecutor executor =
113+
ProcessExecutor.getInstance(ProcessExecutor.Processes.INSTALL_APP);
114+
try {
115+
ProcessExecutorResult result =
116+
executor.runCommandWithOutputHandling(command, tempDir.toFile());
117+
if (result.getRc() != 0) {
118+
throw new IOException("RAR command failed: " + result.getMessages());
119+
}
120+
} catch (InterruptedException e) {
121+
Thread.currentThread().interrupt();
122+
throw new IOException("RAR command interrupted", e);
123+
}
124+
125+
if (!Files.exists(rarFile)) {
126+
throw new IOException("RAR file was not created");
127+
}
128+
129+
try (FileInputStream fis = new FileInputStream(rarFile.toFile());
130+
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
131+
fis.transferTo(baos);
132+
return baos.toByteArray();
133+
}
134+
}
135+
136+
private static void cleanupTempFiles(List<Path> images, Path tempDir) {
137+
for (Path image : images) {
138+
try {
139+
Files.deleteIfExists(image);
140+
} catch (IOException e) {
141+
log.warn("Failed to delete temp image file {}: {}", image, e.getMessage());
142+
}
143+
}
144+
if (tempDir != null) {
145+
try (var paths = Files.walk(tempDir)) {
146+
paths.sorted(Comparator.reverseOrder())
147+
.forEach(
148+
path -> {
149+
try {
150+
Files.deleteIfExists(path);
151+
} catch (IOException e) {
152+
log.warn(
153+
"Failed to delete temp path {}: {}",
154+
path,
155+
e.getMessage());
156+
}
157+
});
158+
} catch (IOException e) {
159+
log.warn("Failed to clean up temp directory {}: {}", tempDir, e.getMessage());
160+
}
87161
}
88162
}
89163

app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ public void init() {
386386
addEndpointToGroup("Java", "pdf-to-markdown");
387387
addEndpointToGroup("Java", "add-attachments");
388388
addEndpointToGroup("Java", "compress-pdf");
389+
addEndpointToGroup("rar", "pdf-to-cbr");
389390

390391
// Javascript
391392
addEndpointToGroup("Javascript", "pdf-organizer");
@@ -484,7 +485,8 @@ private boolean isToolGroup(String group) {
484485
|| "Java".equals(group)
485486
|| "Javascript".equals(group)
486487
|| "Weasyprint".equals(group)
487-
|| "Pdftohtml".equals(group);
488+
|| "Pdftohtml".equals(group)
489+
|| "rar".equals(group);
488490
}
489491

490492
private boolean isEndpointEnabledDirectly(String endpoint) {

app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public ExternalAppDepConfig(
4343
put(unoconvPath, List.of("Unoconvert"));
4444
put("qpdf", List.of("qpdf"));
4545
put("tesseract", List.of("tesseract"));
46+
put("rar", List.of("rar")); // Required for real CBR output
4647
}
4748
};
4849
}
@@ -120,6 +121,7 @@ public void checkDependencies() {
120121
checkDependencyAndDisableGroup(weasyprintPath);
121122
checkDependencyAndDisableGroup("pdftohtml");
122123
checkDependencyAndDisableGroup(unoconvPath);
124+
checkDependencyAndDisableGroup("rar");
123125
// Special handling for Python/OpenCV dependencies
124126
boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python");
125127
if (!pythonAvailable) {

app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,8 @@ public ResponseEntity<?> convertCbrToPdf(@ModelAttribute ConvertCbrToPdfRequest
371371
@Operation(
372372
summary = "Convert PDF to CBR comic book archive",
373373
description =
374-
"This endpoint converts a PDF file to a CBR-like (ZIP-based) comic book archive. "
375-
+ "Note: Output is ZIP-based for compatibility. Input:PDF Output:CBR Type:SISO")
374+
"This endpoint converts a PDF file to a CBR comic book archive using the local RAR CLI. "
375+
+ "Input:PDF Output:CBR Type:SISO")
376376
public ResponseEntity<?> convertPdfToCbr(@ModelAttribute ConvertPdfToCbrRequest request)
377377
throws IOException {
378378
MultipartFile file = request.getFileInput();

app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package stirling.software.SPDF.controller.web;
22

3+
import org.springframework.http.HttpStatus;
34
import org.springframework.stereotype.Controller;
45
import org.springframework.ui.Model;
56
import org.springframework.web.bind.annotation.GetMapping;
7+
import org.springframework.web.server.ResponseStatusException;
68
import org.springframework.web.servlet.ModelAndView;
79

810
import io.swagger.v3.oas.annotations.Hidden;
911
import io.swagger.v3.oas.annotations.tags.Tag;
1012

13+
import stirling.software.SPDF.config.EndpointConfiguration;
1114
import stirling.software.common.model.ApplicationProperties;
1215
import stirling.software.common.util.ApplicationContextProvider;
1316
import stirling.software.common.util.CheckProgramInstall;
@@ -47,6 +50,10 @@ public String convertCbrToPdfForm(Model model) {
4750
@GetMapping("/pdf-to-cbr")
4851
@Hidden
4952
public String convertPdfToCbrForm(Model model) {
53+
if (!ApplicationContextProvider.getBean(EndpointConfiguration.class)
54+
.isEndpointEnabled("pdf-to-cbr")) {
55+
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
56+
}
5057
model.addAttribute("currentPage", "pdf-to-cbr");
5158
return "convert/pdf-to-cbr";
5259
}

app/proprietary/src/main/java/stirling/software/proprietary/security/InitialSecuritySetup.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ private void configureJWTSettings() {
6262

6363
boolean jwtEnabled = jwtProperties.isEnabled();
6464
if (!v2Enabled || !jwtEnabled) {
65-
log.debug("V2 enabled: {}, JWT enabled: {} - disabling all JWT features", v2Enabled, jwtEnabled);
65+
log.debug(
66+
"V2 enabled: {}, JWT enabled: {} - disabling all JWT features",
67+
v2Enabled,
68+
jwtEnabled);
6669

6770
jwtProperties.setKeyCleanup(false);
6871
}

0 commit comments

Comments
 (0)