diff --git a/app/common/src/main/java/stirling/software/common/util/PdfToCbrUtils.java b/app/common/src/main/java/stirling/software/common/util/PdfToCbrUtils.java index a5bf9a888..cb17429ff 100644 --- a/app/common/src/main/java/stirling/software/common/util/PdfToCbrUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/PdfToCbrUtils.java @@ -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 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 images) throws IOException { + List 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 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()); + } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index acee3db0c..2b074640d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -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) { diff --git a/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java index 49129c548..fd3ab640d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java @@ -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) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java index 2ff3b5196..b58b5ea07 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java @@ -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(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java index befde0bbd..970e0719a 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java @@ -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"; } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/InitialSecuritySetup.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/InitialSecuritySetup.java index 1629bb619..2320ea8c6 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/InitialSecuritySetup.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/InitialSecuritySetup.java @@ -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); }