feat(cbz-to-pdf,pdf-to-cbz): Converter for CBZ format to and from PDF (#4472)

This commit is contained in:
Balázs Szücs
2025-09-28 21:41:50 +01:00
committed by GitHub
parent 413cd0c697
commit 51aa03b256
12 changed files with 737 additions and 2 deletions
@@ -0,0 +1,201 @@
package stirling.software.common.util;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.List;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import org.apache.commons.io.FilenameUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.springframework.web.multipart.MultipartFile;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.service.CustomPDFDocumentFactory;
@Slf4j
@UtilityClass
public class CbzUtils {
private final Pattern IMAGE_PATTERN =
Pattern.compile(".*\\.(jpg|jpeg|png|gif|bmp|webp)$", Pattern.CASE_INSENSITIVE);
public byte[] convertCbzToPdf(
MultipartFile cbzFile,
CustomPDFDocumentFactory pdfDocumentFactory,
TempFileManager tempFileManager)
throws IOException {
validateCbzFile(cbzFile);
try (TempFile tempFile = new TempFile(tempFileManager, ".cbz")) {
cbzFile.transferTo(tempFile.getFile());
// Early ZIP validity check using ZipInputStream (fail fast on non-zip content)
try (BufferedInputStream bis =
new BufferedInputStream(
new java.io.FileInputStream(tempFile.getFile()));
ZipInputStream zis = new ZipInputStream(bis)) {
if (zis.getNextEntry() == null) {
throw new IllegalArgumentException("Archive is empty or invalid ZIP");
}
} catch (IOException e) {
throw new IllegalArgumentException("Invalid CBZ/ZIP archive", e);
}
try (PDDocument document = pdfDocumentFactory.createNewDocument();
ZipFile zipFile = new ZipFile(tempFile.getFile())) {
Enumeration<? extends ZipEntry> entries = zipFile.entries();
List<ImageEntryData> imageEntries = new ArrayList<>();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (!entry.isDirectory() && isImageFile(entry.getName())) {
try (InputStream is = zipFile.getInputStream(entry)) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
is.transferTo(baos);
imageEntries.add(
new ImageEntryData(entry.getName(), baos.toByteArray()));
} catch (IOException e) {
log.warn("Error reading image {}: {}", entry.getName(), e.getMessage());
}
}
}
imageEntries.sort(
Comparator.comparing(ImageEntryData::name, new NaturalOrderComparator()));
if (imageEntries.isEmpty()) {
throw new IllegalArgumentException("No valid images found in the CBZ file");
}
for (ImageEntryData imageEntry : imageEntries) {
try {
PDImageXObject pdImage =
PDImageXObject.createFromByteArray(
document, imageEntry.data(), imageEntry.name());
PDPage page =
new PDPage(
new PDRectangle(pdImage.getWidth(), pdImage.getHeight()));
document.addPage(page);
try (PDPageContentStream contentStream =
new PDPageContentStream(document, page)) {
contentStream.drawImage(pdImage, 0, 0);
}
} catch (IOException e) {
log.warn(
"Error processing image {}: {}", imageEntry.name(), e.getMessage());
}
}
if (document.getNumberOfPages() == 0) {
throw new IllegalArgumentException(
"No images could be processed from the CBZ file");
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
return baos.toByteArray();
}
}
}
private void validateCbzFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("File cannot be null or empty");
}
String filename = file.getOriginalFilename();
if (filename == null) {
throw new IllegalArgumentException("File must have a name");
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
if (!"cbz".equals(extension) && !"zip".equals(extension)) {
throw new IllegalArgumentException("File must be a CBZ or ZIP archive");
}
}
public boolean isCbzFile(MultipartFile file) {
String filename = file.getOriginalFilename();
if (filename == null) {
return false;
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
return "cbz".equals(extension) || "zip".equals(extension);
}
private boolean isImageFile(String filename) {
return IMAGE_PATTERN.matcher(filename).matches();
}
private record ImageEntryData(String name, byte[] data) {}
private class NaturalOrderComparator implements Comparator<String> {
@Override
public int compare(String s1, String s2) {
int len1 = s1.length();
int len2 = s2.length();
int marker1 = 0, marker2 = 0;
while (marker1 < len1 && marker2 < len2) {
String chunk1 = getChunk(s1, len1, marker1);
marker1 += chunk1.length();
String chunk2 = getChunk(s2, len2, marker2);
marker2 += chunk2.length();
int result;
if (isDigit(chunk1.charAt(0)) && isDigit(chunk2.charAt(0))) {
int thisNumericValue = Integer.parseInt(chunk1);
int thatNumericValue = Integer.parseInt(chunk2);
result = Integer.compare(thisNumericValue, thatNumericValue);
} else {
result = chunk1.compareTo(chunk2);
}
if (result != 0) {
return result;
}
}
return Integer.compare(len1, len2);
}
private static String getChunk(String s, int length, int marker) {
StringBuilder chunk = new StringBuilder();
char c = s.charAt(marker);
chunk.append(c);
marker++;
if (isDigit(c)) {
while (marker < length && isDigit(s.charAt(marker))) {
chunk.append(s.charAt(marker));
marker++;
}
} else {
while (marker < length && !isDigit(s.charAt(marker))) {
chunk.append(s.charAt(marker));
marker++;
}
}
return chunk.toString();
}
private static boolean isDigit(char ch) {
return ch >= '0' && ch <= '9';
}
}
}
@@ -0,0 +1,99 @@
package stirling.software.common.util;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.imageio.ImageIO;
import org.apache.commons.io.FilenameUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.web.multipart.MultipartFile;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.service.CustomPDFDocumentFactory;
@Slf4j
public class PdfToCbzUtils {
public static byte[] convertPdfToCbz(
MultipartFile pdfFile, int dpi, CustomPDFDocumentFactory pdfDocumentFactory)
throws IOException {
validatePdfFile(pdfFile);
try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
if (document.getNumberOfPages() == 0) {
throw new IllegalArgumentException("PDF file contains no pages");
}
return createCbzFromPdf(document, dpi);
}
}
private static void validatePdfFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("File cannot be null or empty");
}
String filename = file.getOriginalFilename();
if (filename == null) {
throw new IllegalArgumentException("File must have a name");
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
if (!"pdf".equals(extension)) {
throw new IllegalArgumentException("File must be a PDF");
}
}
private static byte[] createCbzFromPdf(PDDocument document, int dpi) throws IOException {
PDFRenderer pdfRenderer = new PDFRenderer(document);
try (ByteArrayOutputStream cbzOutputStream = new ByteArrayOutputStream();
ZipOutputStream zipOut = new ZipOutputStream(cbzOutputStream)) {
int totalPages = document.getNumberOfPages();
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
try {
BufferedImage image =
pdfRenderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB);
String imageFilename = String.format("page_%03d.png", pageIndex + 1);
ZipEntry zipEntry = new ZipEntry(imageFilename);
zipOut.putNextEntry(zipEntry);
ImageIO.write(image, "PNG", zipOut);
zipOut.closeEntry();
} catch (IOException e) {
log.warn("Error processing page {}: {}", pageIndex + 1, e.getMessage());
} catch (OutOfMemoryError e) {
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
} catch (NegativeArraySizeException e) {
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
}
}
zipOut.finish();
return cbzOutputStream.toByteArray();
}
}
public static boolean isPdfFile(MultipartFile file) {
String filename = file.getOriginalFilename();
if (filename == null) {
return false;
}
String extension = FilenameUtils.getExtension(filename).toLowerCase();
return "pdf".equals(extension);
}
}
@@ -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.regex.Pattern;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -16,6 +17,7 @@ import org.apache.commons.io.FileUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.rendering.ImageType;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
@@ -30,11 +32,22 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.converters.ConvertCbzToPdfRequest;
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.*;
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.PdfToCbzUtils;
import stirling.software.common.util.PdfUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.RegexPatternUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/convert")
@@ -44,6 +57,10 @@ import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
public class ConvertImgPDFController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
private static final Pattern EXTENSION_PATTERN =
RegexPatternUtils.getInstance().getPattern(RegexPatternUtils.getExtensionRegex());
private static final String DEFAULT_COMIC_NAME = "comic";
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/img")
@Operation(
@@ -234,6 +251,74 @@ public class ConvertImgPDFController {
GeneralUtils.generateFilename(file[0].getOriginalFilename(), "_converted.pdf"));
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/cbz/pdf")
@Operation(
summary = "Convert CBZ comic book archive to PDF",
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)
throws IOException {
MultipartFile file = request.getFileInput();
byte[] pdfBytes;
try {
pdfBytes = CbzUtils.convertCbzToPdf(file, pdfDocumentFactory, tempFileManager);
} catch (IllegalArgumentException ex) {
String message = ex.getMessage() == null ? "Invalid CBZ file" : ex.getMessage();
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.contentType(MediaType.TEXT_PLAIN)
.body(message.getBytes(java.nio.charset.StandardCharsets.UTF_8));
}
String filename = createConvertedFilename(file.getOriginalFilename(), "_converted.pdf");
return WebResponseUtils.bytesToWebResponse(pdfBytes, filename);
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/cbz")
@Operation(
summary = "Convert PDF to CBZ comic book archive",
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)
throws IOException {
MultipartFile file = request.getFileInput();
Integer dpi = request.getDpi();
if (dpi == null || dpi <= 0) {
dpi = 300;
}
byte[] cbzBytes;
try {
cbzBytes = PdfToCbzUtils.convertPdfToCbz(file, dpi, pdfDocumentFactory);
} catch (IllegalArgumentException ex) {
String message = ex.getMessage() == null ? "Invalid PDF file" : ex.getMessage();
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.contentType(MediaType.TEXT_PLAIN)
.body(message.getBytes(java.nio.charset.StandardCharsets.UTF_8));
}
String filename = createConvertedFilename(file.getOriginalFilename(), "_converted.cbz");
return WebResponseUtils.bytesToWebResponse(
cbzBytes, filename, MediaType.APPLICATION_OCTET_STREAM);
}
private String createConvertedFilename(String originalFilename, String suffix) {
if (originalFilename == null) {
return GeneralUtils.generateFilename(DEFAULT_COMIC_NAME, suffix);
}
String baseName = EXTENSION_PATTERN.matcher(originalFilename).replaceFirst("");
if (baseName.isBlank()) {
baseName = DEFAULT_COMIC_NAME;
}
return GeneralUtils.generateFilename(baseName, suffix);
}
private String getMediaType(String imageFormat) {
String mimeType = URLConnection.guessContentTypeFromName("." + imageFormat);
return "null".equals(mimeType) ? MediaType.APPLICATION_OCTET_STREAM_VALUE : mimeType;
@@ -23,6 +23,20 @@ public class ConverterWebController {
return "convert/img-to-pdf";
}
@GetMapping("/cbz-to-pdf")
@Hidden
public String convertCbzToPdfForm(Model model) {
model.addAttribute("currentPage", "cbz-to-pdf");
return "convert/cbz-to-pdf";
}
@GetMapping("/pdf-to-cbz")
@Hidden
public String convertPdfToCbzForm(Model model) {
model.addAttribute("currentPage", "pdf-to-cbz");
return "convert/pdf-to-cbz";
}
@GetMapping("/html-to-pdf")
@Hidden
public String convertHTMLToPdfForm(Model model) {
@@ -0,0 +1,18 @@
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 ConvertCbzToPdfRequest {
@Schema(
description = "The input CBZ file to be converted to a PDF file",
requiredMode = Schema.RequiredMode.REQUIRED)
private MultipartFile fileInput;
}
@@ -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 ConvertPdfToCbzRequest {
@Schema(
description = "The input PDF file to be converted to a CBZ file",
requiredMode = Schema.RequiredMode.REQUIRED)
private MultipartFile fileInput;
@Schema(
description = "The DPI (Dots Per Inch) for rendering PDF pages as images",
example = "300",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private Integer dpi = 300;
}
@@ -606,6 +606,14 @@ home.imageToPdf.title=Image to PDF
home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF, PSD) to PDF.
imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop
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.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.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
@@ -1431,6 +1439,18 @@ imageToPDF.selectText.3=Multi file logic (Only enabled if working with multiple
imageToPDF.selectText.4=Merge into single PDF
imageToPDF.selectText.5=Convert to separate PDFs
#cbzToPDF
cbzToPDF.title=CBZ to PDF
cbzToPDF.header=CBZ to PDF
cbzToPDF.submit=Convert to PDF
cbzToPDF.selectText=Select CBZ file
#pdfToCBZ
pdfToCBZ.title=PDF to CBZ
pdfToCBZ.header=PDF to CBZ
pdfToCBZ.submit=Convert to CBZ
pdfToCBZ.selectText=Select PDF file
pdfToCBZ.dpi=DPI (Dots Per Inch)
#pdfToImage
pdfToImage.title=PDF to Image
@@ -0,0 +1,39 @@
<!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=#{cbzToPDF.title}, header=#{cbzToPDF.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="#{cbzToPDF.header}"></span>
</div>
<form enctype="multipart/form-data" id="cbzToPDFForm" method="post"
th:action="@{'/api/v1/convert/cbz/pdf'}">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.cbz,.zip', inputText=#{cbzToPDF.selectText})}">
</div>
<br>
<button class="btn btn-primary" id="submitBtn" th:text="#{cbzToPDF.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>
@@ -0,0 +1,42 @@
<!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=#{pdfToCBZ.title}, header=#{pdfToCBZ.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 convert">auto_stories</span>
<span class="tool-header-text" th:text="#{pdfToCBZ.header}"></span>
</div>
<form enctype="multipart/form-data" id="pdfToCBZForm" method="post"
th:action="@{'/api/v1/convert/pdf/cbz'}">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf', inputText=#{pdfToCBZ.selectText})}">
</div>
<div class="mb-3">
<label for="dpi" th:text="#{pdfToCBZ.dpi}">DPI:</label>
<input class="form-control" id="dpi" max="600" min="72" name="dpi" required step="1" type="number" value="300">
</div>
<br>
<button class="btn btn-primary" id="submitBtn" th:text="#{pdfToCBZ.submit}" type="submit">Convert to CBZ</button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
@@ -47,6 +47,9 @@
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('img-to-pdf', 'picture_as_pdf', 'home.imageToPdf.title', 'home.imageToPdf.desc', 'imageToPdf.tags', 'convertto')}">
</div>
<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('file-to-pdf', 'draft', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags', 'convertto')}">
</div>
@@ -71,6 +74,9 @@
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-img', 'photo_library', 'home.pdfToImage.title', 'home.pdfToImage.desc', 'pdfToImage.tags', 'convert')}">
</div>
<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-pdfa', 'picture_as_pdf', 'home.pdfToPDFA.title', 'home.pdfToPDFA.desc', 'pdfToPDFA.tags', 'convert')}">
</div>
@@ -98,13 +104,16 @@
</div>
</div>
<div id="convertGroup" class="feature-group">
<div class="feature-group" id="convertGroup">
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.convertTo})}">
</div>
<div class="nav-group-container">
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('img-to-pdf', 'picture_as_pdf', 'home.imageToPdf.title', 'home.imageToPdf.desc', 'imageToPdf.tags', 'convertto')}">
</div>
<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('file-to-pdf', 'draft', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags', 'convertto')}">
</div>
@@ -0,0 +1,84 @@
package stirling.software.SPDF.controller.api.converters;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockMultipartFile;
import stirling.software.common.util.CbzUtils;
class CbzUtilsTest {
@Test
void testIsCbzFile_ValidCbzFile() {
MockMultipartFile cbzFile =
new MockMultipartFile(
"file", "test.cbz", "application/zip", "test content".getBytes());
assertTrue(CbzUtils.isCbzFile(cbzFile));
}
@Test
void testIsCbzFile_ValidZipFile() {
MockMultipartFile zipFile =
new MockMultipartFile(
"file", "test.zip", "application/zip", "test content".getBytes());
assertTrue(CbzUtils.isCbzFile(zipFile));
}
@Test
void testIsCbzFile_InvalidFile() {
MockMultipartFile textFile =
new MockMultipartFile("file", "test.txt", "text/plain", "test content".getBytes());
assertFalse(CbzUtils.isCbzFile(textFile));
}
@Test
void testIsCbzFile_NoFilename() {
MockMultipartFile noNameFile =
new MockMultipartFile("file", null, "application/zip", "test content".getBytes());
assertFalse(CbzUtils.isCbzFile(noNameFile));
}
@Test
void testIsCbzFile_PdfFile() {
MockMultipartFile pdfFile =
new MockMultipartFile(
"file", "document.pdf", "application/pdf", "pdf content".getBytes());
assertFalse(CbzUtils.isCbzFile(pdfFile));
}
@Test
void testIsCbzFile_JpegFile() {
MockMultipartFile jpegFile =
new MockMultipartFile("file", "image.jpg", "image/jpeg", "jpeg content".getBytes());
assertFalse(CbzUtils.isCbzFile(jpegFile));
}
@Test
void testIsCbzFile_RarFile() {
MockMultipartFile rarFile =
new MockMultipartFile(
"file",
"archive.rar",
"application/x-rar-compressed",
"rar content".getBytes());
assertFalse(CbzUtils.isCbzFile(rarFile));
}
@Test
void testIsCbzFile_MixedCaseExtension() {
MockMultipartFile cbzFile =
new MockMultipartFile(
"file", "test.CBZ", "application/zip", "test content".getBytes());
assertTrue(CbzUtils.isCbzFile(cbzFile));
}
}
@@ -0,0 +1,100 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.IOException;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.mock.web.MockMultipartFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.PdfToCbzUtils;
public class PdfToCbzUtilsTest {
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
public void testIsPdfFile() {
MockMultipartFile pdfFile =
new MockMultipartFile("test", "test.pdf", "application/pdf", new byte[10]);
MockMultipartFile nonPdfFile =
new MockMultipartFile("test", "test.txt", "text/plain", new byte[10]);
MockMultipartFile noNameFile =
new MockMultipartFile("test", null, "application/pdf", new byte[10]);
Assertions.assertTrue(PdfToCbzUtils.isPdfFile(pdfFile));
Assertions.assertFalse(PdfToCbzUtils.isPdfFile(nonPdfFile));
Assertions.assertFalse(PdfToCbzUtils.isPdfFile(noNameFile));
}
@Test
public void testConvertPdfToCbz_NullFile() {
IllegalArgumentException exception =
Assertions.assertThrows(
IllegalArgumentException.class,
() -> {
PdfToCbzUtils.convertPdfToCbz(null, 300, pdfDocumentFactory);
});
Assertions.assertEquals("File cannot be null or empty", exception.getMessage());
}
@Test
public void testConvertPdfToCbz_EmptyFile() {
MockMultipartFile emptyFile =
new MockMultipartFile("test", "test.pdf", "application/pdf", new byte[0]);
IllegalArgumentException exception =
Assertions.assertThrows(
IllegalArgumentException.class,
() -> {
PdfToCbzUtils.convertPdfToCbz(emptyFile, 300, pdfDocumentFactory);
});
Assertions.assertEquals("File cannot be null or empty", exception.getMessage());
}
@Test
public void testConvertPdfToCbz_NonPdfFile() {
MockMultipartFile nonPdfFile =
new MockMultipartFile("test", "test.txt", "text/plain", new byte[10]);
IllegalArgumentException exception =
Assertions.assertThrows(
IllegalArgumentException.class,
() -> {
PdfToCbzUtils.convertPdfToCbz(nonPdfFile, 300, pdfDocumentFactory);
});
Assertions.assertEquals("File must be a PDF", exception.getMessage());
}
@Test
public void testConvertPdfToCbz_ValidPdf() throws IOException {
// Create a simple mock PDF
MockMultipartFile pdfFile =
new MockMultipartFile("test", "test.pdf", "application/pdf", new byte[100]);
// Mock the PDF document
PDDocument mockDocument = Mockito.mock(PDDocument.class);
Mockito.when(mockDocument.getNumberOfPages()).thenReturn(1);
Mockito.when(pdfDocumentFactory.load(pdfFile)).thenReturn(mockDocument);
// structure
Assertions.assertThrows(
Exception.class,
() -> {
PdfToCbzUtils.convertPdfToCbz(pdfFile, 300, pdfDocumentFactory);
});
// Verify that load was called
Mockito.verify(pdfDocumentFactory).load(pdfFile);
}
}