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
This commit is contained in:
Harsh Vardhan
2025-12-14 09:53:19 +05:30
committed by GitHub
parent 708e851e0b
commit 9da13ae818
9 changed files with 45 additions and 20 deletions

View File

@@ -11,4 +11,5 @@ public class KoboSettings {
private boolean convertCbxToEpub;
private int conversionLimitInMbForCbx;
private boolean forceEnableHyphenation;
private int conversionImageCompressionPercentage;
}

View File

@@ -255,6 +255,7 @@ public class SettingPersistenceHelper {
.conversionLimitInMb(100)
.convertCbxToEpub(false)
.conversionLimitInMbForCbx(100)
.conversionImageCompressionPercentage(85)
.forceEnableHyphenation(false)
.build();
}

View File

@@ -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) {

View File

@@ -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<EpubContentFileGroup> contentGroups = addImagesAndPages(zipOut, imagePaths);
List<EpubContentFileGroup> contentGroups = addImagesAndPages(zipOut, imagePaths,compressionPercentage);
addContentOpf(zipOut, bookEntity, contentGroups);
addTocNcx(zipOut, bookEntity, contentGroups);
@@ -340,13 +340,13 @@ public class CbxConversionService {
zipOut.closeArchiveEntry();
}
private List<EpubContentFileGroup> addImagesAndPages(ZipArchiveOutputStream zipOut, List<Path> imagePaths)
private List<EpubContentFileGroup> addImagesAndPages(ZipArchiveOutputStream zipOut, List<Path> imagePaths,int compressionPercentage)
throws IOException, TemplateException {
List<EpubContentFileGroup> 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)) {

View File

@@ -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()

View File

@@ -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);

View File

@@ -263,7 +263,26 @@
</p>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label-row">
<label class="setting-label">Conversion image compression: {{ koboSettings.conversionImageCompressionPercentage }}%</label>
<div class="slider-container">
<p-slider
id="conversionLimit"
[(ngModel)]="koboSettings.conversionImageCompressionPercentage"
[min]="1"
[max]="100"
[step]="1"
(ngModelChange)="onSliderChange()">
</p-slider>
</div>
</div>
<p class="setting-description">
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.
</p>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label-row">

View File

@@ -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;
});
}

View File

@@ -102,6 +102,7 @@ export interface PublicReviewSettings {
export interface KoboSettings {
convertToKepub: boolean;
conversionLimitInMb: number;
conversionImageCompressionPercentage: number;
convertCbxToEpub: boolean;
conversionLimitInMbForCbx: number;
forceEnableHyphenation: boolean;