From 9da13ae81806fcb9edec5616a9d430c99e3e9dbe Mon Sep 17 00:00:00 2001 From: Harsh Vardhan Date: Sun, 14 Dec 2025 09:53:19 +0530 Subject: [PATCH] Feat/conversion CBX to EPUB compression configuration (#1844) * feat(conversion): add image compression percentage setting for CBX to EPUB conversion * feat(conversion): add conversion image compression setting to kobo sync settings frontend --- .../model/dto/settings/KoboSettings.java | 1 + .../appsettings/SettingPersistenceHelper.java | 1 + .../service/book/BookDownloadService.java | 3 ++- .../service/kobo/CbxConversionService.java | 18 ++++++++-------- .../kobo/CbxConversionIntegrationTest.java | 2 +- .../kobo/CbxConversionServiceTest.java | 16 +++++++------- .../kobo-sync-settings-component.html | 21 ++++++++++++++++++- .../kobo-sync-settings-component.ts | 2 ++ .../app/shared/model/app-settings.model.ts | 1 + 9 files changed, 45 insertions(+), 20 deletions(-) diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/KoboSettings.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/KoboSettings.java index 1a6f1138..47c0e1f2 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/KoboSettings.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/KoboSettings.java @@ -11,4 +11,5 @@ public class KoboSettings { private boolean convertCbxToEpub; private int conversionLimitInMbForCbx; private boolean forceEnableHyphenation; + private int conversionImageCompressionPercentage; } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/appsettings/SettingPersistenceHelper.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/appsettings/SettingPersistenceHelper.java index 9548deee..d1a0a6b1 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/appsettings/SettingPersistenceHelper.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/appsettings/SettingPersistenceHelper.java @@ -255,6 +255,7 @@ public class SettingPersistenceHelper { .conversionLimitInMb(100) .convertCbxToEpub(false) .conversionLimitInMbForCbx(100) + .conversionImageCompressionPercentage(85) .forceEnableHyphenation(false) .build(); } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/book/BookDownloadService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/book/BookDownloadService.java index a5b93542..3ba1ae69 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/book/BookDownloadService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/book/BookDownloadService.java @@ -96,6 +96,7 @@ public class BookDownloadService { boolean convertEpubToKepub = isEpub && koboSettings.isConvertToKepub() && bookEntity.getFileSizeKb() <= (long) koboSettings.getConversionLimitInMb() * 1024; boolean convertCbxToEpub = isCbx && koboSettings.isConvertCbxToEpub() && bookEntity.getFileSizeKb() <= (long) koboSettings.getConversionLimitInMbForCbx() * 1024; + int compressionPercentage = koboSettings.getConversionImageCompressionPercentage(); Path tempDir = null; try { File inputFile = new File(FileUtils.getBookFullPath(bookEntity)); @@ -106,7 +107,7 @@ public class BookDownloadService { } if (convertCbxToEpub) { - fileToSend = cbxConversionService.convertCbxToEpub(inputFile, tempDir.toFile(), bookEntity); + fileToSend = cbxConversionService.convertCbxToEpub(inputFile, tempDir.toFile(), bookEntity,compressionPercentage); } if (convertEpubToKepub) { diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/kobo/CbxConversionService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/kobo/CbxConversionService.java index 0215559d..31a3ad51 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/kobo/CbxConversionService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/kobo/CbxConversionService.java @@ -102,20 +102,20 @@ public class CbxConversionService { * @throws IllegalArgumentException if the file format is not supported * @throws IllegalStateException if no valid images are found in the archive */ - public File convertCbxToEpub(File cbxFile, File tempDir, BookEntity bookEntity) + public File convertCbxToEpub(File cbxFile, File tempDir, BookEntity bookEntity, int compressionPercentage) throws IOException, TemplateException, RarException { validateInputs(cbxFile, tempDir); log.info("Starting CBX to EPUB conversion for: {}", cbxFile.getName()); - File outputFile = executeCbxConversion(cbxFile, tempDir, bookEntity); + File outputFile = executeCbxConversion(cbxFile, tempDir, bookEntity,compressionPercentage); log.info("Successfully converted {} to {} (size: {} bytes)", cbxFile.getName(), outputFile.getName(), outputFile.length()); return outputFile; } - private File executeCbxConversion(File cbxFile, File tempDir, BookEntity bookEntity) + private File executeCbxConversion(File cbxFile, File tempDir, BookEntity bookEntity,int compressionPercentage) throws IOException, TemplateException, RarException { Path epubFilePath = Paths.get(tempDir.getAbsolutePath(), cbxFile.getName() + ".epub"); @@ -136,7 +136,7 @@ public class CbxConversionService { addMetaInfContainer(zipOut); addStylesheet(zipOut); - List contentGroups = addImagesAndPages(zipOut, imagePaths); + List contentGroups = addImagesAndPages(zipOut, imagePaths,compressionPercentage); addContentOpf(zipOut, bookEntity, contentGroups); addTocNcx(zipOut, bookEntity, contentGroups); @@ -340,13 +340,13 @@ public class CbxConversionService { zipOut.closeArchiveEntry(); } - private List addImagesAndPages(ZipArchiveOutputStream zipOut, List imagePaths) + private List addImagesAndPages(ZipArchiveOutputStream zipOut, List imagePaths,int compressionPercentage) throws IOException, TemplateException { List contentGroups = new ArrayList<>(); if (!imagePaths.isEmpty()) { - addImageToZipFromPath(zipOut, COVER_IMAGE_PATH, imagePaths.getFirst()); + addImageToZipFromPath(zipOut, COVER_IMAGE_PATH, imagePaths.getFirst(),compressionPercentage); } for (int i = 0; i < imagePaths.size(); i++) { @@ -358,7 +358,7 @@ public class CbxConversionService { String imagePath = IMAGE_ROOT_PATH + imageFileName; String htmlPath = HTML_ROOT_PATH + htmlFileName; - addImageToZipFromPath(zipOut, imagePath, imageSourcePath); + addImageToZipFromPath(zipOut, imagePath, imageSourcePath,compressionPercentage); String htmlContent = generatePageHtml(imageFileName, i + 1); ZipArchiveEntry htmlEntry = new ZipArchiveEntry(htmlPath); @@ -372,7 +372,7 @@ public class CbxConversionService { return contentGroups; } - private void addImageToZipFromPath(ZipArchiveOutputStream zipOut, String epubImagePath, Path sourceImagePath) + private void addImageToZipFromPath(ZipArchiveOutputStream zipOut, String epubImagePath, Path sourceImagePath,int compressionPercentage) throws IOException { ZipArchiveEntry imageEntry = new ZipArchiveEntry(epubImagePath); zipOut.putArchiveEntry(imageEntry); @@ -385,7 +385,7 @@ public class CbxConversionService { try (InputStream fis = Files.newInputStream(sourceImagePath)) { BufferedImage image = ImageIO.read(fis); if (image != null) { - writeJpegImage(image, zipOut, 0.85f); + writeJpegImage(image, zipOut, compressionPercentage/100f); } else { log.warn("Could not decode image {}, copying raw bytes", sourceImagePath.getFileName()); try (InputStream rawStream = Files.newInputStream(sourceImagePath)) { diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/service/kobo/CbxConversionIntegrationTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/service/kobo/CbxConversionIntegrationTest.java index 26c5fbd1..6910f99c 100644 --- a/booklore-api/src/test/java/com/adityachandel/booklore/service/kobo/CbxConversionIntegrationTest.java +++ b/booklore-api/src/test/java/com/adityachandel/booklore/service/kobo/CbxConversionIntegrationTest.java @@ -42,7 +42,7 @@ class CbxConversionIntegrationTest { File testCbzFile = createTestComicCbzFile(); BookEntity bookMetadata = createTestBookMetadata(); - File epubFile = conversionService.convertCbxToEpub(testCbzFile, tempDir.toFile(), bookMetadata); + File epubFile = conversionService.convertCbxToEpub(testCbzFile, tempDir.toFile(), bookMetadata,85); assertThat(epubFile) .exists() diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/service/kobo/CbxConversionServiceTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/service/kobo/CbxConversionServiceTest.java index dfbad12a..ed4d4170 100644 --- a/booklore-api/src/test/java/com/adityachandel/booklore/service/kobo/CbxConversionServiceTest.java +++ b/booklore-api/src/test/java/com/adityachandel/booklore/service/kobo/CbxConversionServiceTest.java @@ -42,7 +42,7 @@ class CbxConversionServiceTest { @Test void convertCbxToEpub_WithValidCbzFile_ShouldGenerateValidEpub() throws IOException, TemplateException, RarException { - File epubFile = cbxConversionService.convertCbxToEpub(testCbzFile, tempDir.toFile(), testBookEntity); + File epubFile = cbxConversionService.convertCbxToEpub(testCbzFile, tempDir.toFile(), testBookEntity,85); assertThat(epubFile).exists(); assertThat(epubFile.getName()).endsWith(".epub"); @@ -53,7 +53,7 @@ class CbxConversionServiceTest { @Test void convertCbxToEpub_WithNullCbxFile_ShouldThrowException() { - assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(null, tempDir.toFile(), testBookEntity)) + assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(null, tempDir.toFile(), testBookEntity,85)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Invalid CBX file"); } @@ -62,7 +62,7 @@ class CbxConversionServiceTest { void convertCbxToEpub_WithNonExistentFile_ShouldThrowException() { File nonExistentFile = new File(tempDir.toFile(), "non-existent.cbz"); - assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(nonExistentFile, tempDir.toFile(), testBookEntity)) + assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(nonExistentFile, tempDir.toFile(), testBookEntity,85)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Invalid CBX file"); } @@ -71,14 +71,14 @@ class CbxConversionServiceTest { void convertCbxToEpub_WithUnsupportedFileFormat_ShouldThrowException() throws IOException { File unsupportedFile = Files.createFile(tempDir.resolve("test.txt")).toFile(); - assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(unsupportedFile, tempDir.toFile(), testBookEntity)) + assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(unsupportedFile, tempDir.toFile(), testBookEntity,85)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Unsupported file format"); } @Test void convertCbxToEpub_WithNullTempDir_ShouldThrowException() { - assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(testCbzFile, null, testBookEntity)) + assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(testCbzFile, null, testBookEntity,85)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Invalid temp directory"); } @@ -87,7 +87,7 @@ class CbxConversionServiceTest { void convertCbxToEpub_WithEmptyCbzFile_ShouldThrowException() throws IOException { File emptyCbzFile = createEmptyCbzFile(); - assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(emptyCbzFile, tempDir.toFile(), testBookEntity)) + assertThatThrownBy(() -> cbxConversionService.convertCbxToEpub(emptyCbzFile, tempDir.toFile(), testBookEntity,85)) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("No valid images found"); } @@ -118,7 +118,7 @@ class CbxConversionServiceTest { @Test void convertCbxToEpub_WithNullBookEntity_ShouldUseDefaultMetadata() throws IOException, TemplateException, RarException { - File epubFile = cbxConversionService.convertCbxToEpub(testCbzFile, tempDir.toFile(), null); + File epubFile = cbxConversionService.convertCbxToEpub(testCbzFile, tempDir.toFile(), null,85); assertThat(epubFile).exists(); verifyEpubStructure(epubFile); @@ -128,7 +128,7 @@ class CbxConversionServiceTest { void convertCbxToEpub_WithMultipleImages_ShouldPreservePageOrder() throws IOException, TemplateException, RarException { File multiPageCbzFile = createMultiPageCbzFile(); - File epubFile = cbxConversionService.convertCbxToEpub(multiPageCbzFile, tempDir.toFile(), testBookEntity); + File epubFile = cbxConversionService.convertCbxToEpub(multiPageCbzFile, tempDir.toFile(), testBookEntity,85); assertThat(epubFile).exists(); verifyPageOrderInEpub(epubFile, 5); diff --git a/booklore-ui/src/app/features/settings/device-settings/component/kobo-sync-settings/kobo-sync-settings-component.html b/booklore-ui/src/app/features/settings/device-settings/component/kobo-sync-settings/kobo-sync-settings-component.html index d3631097..134a7328 100644 --- a/booklore-ui/src/app/features/settings/device-settings/component/kobo-sync-settings/kobo-sync-settings-component.html +++ b/booklore-ui/src/app/features/settings/device-settings/component/kobo-sync-settings/kobo-sync-settings-component.html @@ -263,7 +263,26 @@

- +
+
+
+ +
+ + +
+
+

+ Comic book conversions can sometimes result in very large files. This setting allows you to compress the images during conversion to prevent size from shooting up. +

+
+
diff --git a/booklore-ui/src/app/features/settings/device-settings/component/kobo-sync-settings/kobo-sync-settings-component.ts b/booklore-ui/src/app/features/settings/device-settings/component/kobo-sync-settings/kobo-sync-settings-component.ts index 0847e345..15163e3d 100644 --- a/booklore-ui/src/app/features/settings/device-settings/component/kobo-sync-settings/kobo-sync-settings-component.ts +++ b/booklore-ui/src/app/features/settings/device-settings/component/kobo-sync-settings/kobo-sync-settings-component.ts @@ -48,6 +48,7 @@ export class KoboSyncSettingsComponent implements OnInit, OnDestroy { convertToKepub: false, conversionLimitInMb: 100, convertCbxToEpub: false, + conversionImageCompressionPercentage: 85, conversionLimitInMbForCbx: 100, forceEnableHyphenation: false }; @@ -138,6 +139,7 @@ export class KoboSyncSettingsComponent implements OnInit, OnDestroy { this.koboSettings.convertCbxToEpub = settings?.koboSettings?.convertCbxToEpub ?? false; this.koboSettings.conversionLimitInMbForCbx = settings?.koboSettings?.conversionLimitInMbForCbx ?? 100; this.koboSettings.forceEnableHyphenation = settings?.koboSettings?.forceEnableHyphenation ?? false; + this.koboSettings.conversionImageCompressionPercentage = settings?.koboSettings?.conversionImageCompressionPercentage ?? 85; }); } diff --git a/booklore-ui/src/app/shared/model/app-settings.model.ts b/booklore-ui/src/app/shared/model/app-settings.model.ts index ab1b51c3..13168f50 100644 --- a/booklore-ui/src/app/shared/model/app-settings.model.ts +++ b/booklore-ui/src/app/shared/model/app-settings.model.ts @@ -102,6 +102,7 @@ export interface PublicReviewSettings { export interface KoboSettings { convertToKepub: boolean; conversionLimitInMb: number; + conversionImageCompressionPercentage: number; convertCbxToEpub: boolean; conversionLimitInMbForCbx: number; forceEnableHyphenation: boolean;