Compare commits

...
12 Commits
Author SHA1 Message Date
Alex Xu 38fa13610f A "Covers" folder next to source files defines custom covers (#1432)
* A "Covers" folder next to source files defines custom covers

* fix case with output folder unchecked
2026-09-03 15:00:07 -07:00
Alex Xu 6d3bd0296c add folder output to readme 2026-09-03 10:31:36 -07:00
Alex Xu 9decf0e68e add wallpaper mode for KOreader (#1431)
* add wallpaper mode for KOreader

* fix wallpaper interactions with no rotate
2026-09-03 10:21:08 -07:00
Alex Xu dc2cc8e7b8 rename folder to folder of images 2026-09-02 19:49:13 -07:00
Alex Xu 55468019dd add folder output option (#1430) 2026-09-02 16:56:42 -07:00
Alex Xu 8f191e604b replace more images 2026-08-30 18:12:27 -07:00
Alex Xu 924f83d976 use smaller images 2026-08-30 18:09:30 -07:00
Alex Xu 52d52ff323 Update README.md 2026-08-29 16:21:41 -07:00
Alex Xu e93be5cf68 Add files via upload 2026-08-29 16:21:20 -07:00
Alex Xu 4cdbb4beef bump to 11.0.3 2026-08-28 14:52:42 -07:00
Alex Xu 2c9056c524 label spread preview default is high quality (#1429) 2026-08-28 14:46:18 -07:00
Alex Xu a503f54f9f Spread labels support left to right (#1428) 2026-08-28 14:38:22 -07:00
14 changed files with 110 additions and 24 deletions
+8 -7
View File
@@ -1,6 +1,6 @@
Most manga conversion methods for ereaders add margins. But KCC enables true fullscreen from top to bottom.
![header](images/header_margins.jpeg)
![header](images/header_margins720.jpeg)
(feat. Ya Boy Kongming! and Fire Force from a [Humble Bundle](https://humblebundleinc.sjv.io/xL6Zv1) PDF source.)
@@ -19,7 +19,7 @@ KCC runs on Windows, macOS, and Linux.
Supported input formats include JPG/PNG image files in folders, archives like CBZ/EPUB, or PDF.
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, PDF, and folders of images.
The absolute highest quality source files are DRM-free PDFs from [Humble Bundle](https://humblebundleinc.sjv.io/xL6Zv1)/Fanatical.
The PDFs are often x12000 resolution or better, while even the 10" Kindle Scribe is only x2480.
@@ -33,23 +33,23 @@ KCC's main goal is maximum image quality at significantly smaller file size. For
2) fixes black levels to avoid gray/faded blacks found in many Kindle Store manga
![black](images/black.jpeg)
![black](images/black720.jpeg)
3) fixes [the rainbow effect on Kaleido 3 color eink](https://www.youtube.com/watch?v=Dw2HTJCGMhw) without blur:
![rainbow](images/rainbow2.jpeg)
![rainbow](images/rainbow720.jpeg)
4) can [semi-automatically detect 2-page spreads](https://www.youtube.com/watch?v=kfIX67f7Aqk) from pre-split single pages:
![before](images/spreadbefore3.jpeg)
![before](images/spreadbefore720.jpeg)
You can view combined spreads either rotated or not:
![full](images/full.jpeg)
![full](images/full720.jpeg)
You can view the split halves either before or after combined spreads:
![half](images/half.jpeg)
![half](images/half720.jpeg)
## Demo
@@ -287,6 +287,7 @@ PROCESSING:
--pdfwidth Render vector PDFs based on device width instead of height.
-u, --upscale Resize images smaller than device's resolution
-s, --stretch Stretch images to device's resolution
--wallpaper Crop images to to fill screen
-r SPLITTER, --splitter SPLITTER
Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]
-g GAMMA, --gamma GAMMA
+13 -1
View File
@@ -693,6 +693,18 @@ Keeping this file may crash some readers like the Kobo native CBZ reader.</strin
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QCheckBox" name="wallpaperBox">
<property name="toolTip">
<string>Auto check various options intended for KOreader wallpapers.
Will also crop images to fill the screen centered.</string>
</property>
<property name="text">
<string>Wallpaper mode</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -780,7 +792,7 @@ Keeping this file may crash some readers like the Kobo native CBZ reader.</strin
</size>
</property>
<property name="toolTip">
<string>Hold shift while clicking for a higher quality preview.</string>
<string>Hold shift while clicking for a low quality preview.</string>
</property>
<property name="text">
<string>Label Spreads</string>
Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

+31 -7
View File
@@ -127,6 +127,8 @@ class Icons:
self.MOBIFormat.addPixmap(QPixmap(":/Formats/icons/MOBI.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.CBZFormat = QIcon()
self.CBZFormat.addPixmap(QPixmap(":/Formats/icons/CBZ.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.FOLDERFormat = QIcon()
self.FOLDERFormat.addPixmap(QPixmap(":/Other/icons/folder_new.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.EPUBFormat = QIcon()
self.EPUBFormat.addPixmap(QPixmap(":/Formats/icons/EPUB.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.KFXFormat = QIcon()
@@ -251,6 +253,8 @@ def get_options():
options.righttoleft = True
if GUI.lightnovelBox.isChecked():
options.lightnovel = True
if GUI.wallpaperBox.isChecked():
options.wallpaper = True
if GUI.ebokBox.isChecked():
options.ebok = True
if GUI.invertDirectionBox.isChecked():
@@ -442,6 +446,9 @@ class WorkerThread(QThread):
if gui_current_format == 'CBZ':
MW.addMessage.emit('Creating CBZ files', 'info', False)
GUI.progress.content = 'Creating CBZ files'
elif gui_current_format == 'FOLDER':
MW.addMessage.emit('Creating folders', 'info', False)
GUI.progress.content = 'Creating folders'
elif gui_current_format == 'PDF':
MW.addMessage.emit('Creating PDF files', 'info', False)
GUI.progress.content = 'Creating PDF files'
@@ -487,6 +494,8 @@ class WorkerThread(QThread):
GUI.progress.content = ''
if gui_current_format == 'CBZ':
MW.addMessage.emit('Creating CBZ files... <b>Done!</b>', 'info', True)
elif gui_current_format == 'FOLDER':
MW.addMessage.emit('Creating folders... <b>Done!</b>', 'info', True)
elif gui_current_format == 'PDF':
MW.addMessage.emit('Creating PDF files... <b>Done!</b>', 'info', True)
else:
@@ -671,9 +680,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.jobList.scrollToBottom()
def labelSpreadsStart(self):
low_quality_preview = True
low_quality_preview = False
if QApplication.keyboardModifiers() == Qt.ShiftModifier:
low_quality_preview = False
low_quality_preview = True
currentJobs = []
# TODO: make this a function since it's copy pasted
for i in range(GUI.jobList.count()):
@@ -708,19 +717,22 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
# TODO: with statements
# TODO: ignore 1% of top and bottom too?
im1 = Image.open(os.path.join(root, files[i]))
im2 = Image.open(os.path.join(root, files[i+1]))
if not options.righttoleft:
im1, im2 = im2, im1
size1 = im1.size
size2 = im2.size
preview_crop1 = im1.crop((0, 0, 0.2*size1[0], size1[1]))
if low_quality_preview:
preview_crop1 = preview_crop1.convert('1', dither=Dither.NONE)
calculation_crop1 = im1.crop((0.01*size1[0], 0, 0.04*size1[0], size1[1])).convert('1', dither=Dither.NONE)
#crop1 = crop11
im2 = Image.open(os.path.join(root, files[i+1]))
size2 = im2.size
preview_crop2 = im2.crop((0.8*size2[0], 0, size2[0], size2[1]))
if low_quality_preview:
preview_crop2 = preview_crop2.convert('1', dither=Dither.NONE)
calculation_crop2 = im2.crop((0.96*size2[0], 0, .99* size2[0], size2[1])).convert('1', dither=Dither.NONE)
#crop2 = crop22
# dst = Image.new('1', (im1.width + im2.width, im1.height))
# dst.paste(im2, (0, 0))
# dst.paste(im1, (im1.width, 0))
@@ -959,6 +971,14 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.noQuantizeBox.setEnabled(False)
GUI.forcePngRgbBox.setEnabled(False)
def toggleWallpaperBox(self, value):
if value == 2:
GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked)
GUI.pngLegacyBox.setChecked(True)
GUI.rotateBox.setChecked(True)
GUI.noRotateBox.setChecked(True)
GUI.upscaleBox.setChecked(True)
self.changeFormat(3)
def togglechunkSizeCheckBox(self, value):
GUI.chunkSizeWidget.setVisible(value)
@@ -1058,9 +1078,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.upscaleBox.setChecked(True)
elif not GUI.webtoonBox.isChecked():
GUI.chunkSizeCheckBox.setEnabled(True)
if GUI.formats[str(GUI.formatBox.currentText())]['format'] in ('CBZ', 'PDF') and not GUI.webtoonBox.isChecked():
if GUI.formats[str(GUI.formatBox.currentText())]['format'] in ('CBZ', 'FOLDER', 'PDF') and not GUI.webtoonBox.isChecked():
self.addMessage("Partially check W/B Margins if you don't want KCC to extend the image margins.", 'info')
GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked)
else:
GUI.borderBox.setCheckState(Qt.CheckState.Unchecked)
@@ -1175,6 +1196,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.settings.setValue('windowSize', str(MW.size().width()) + 'x' + str(MW.size().height()))
self.settings.setValue('options', {'mangaBox': GUI.mangaBox.checkState(),
'lightnovelBox': GUI.lightnovelBox.checkState(),
'wallpaperBox': GUI.wallpaperBox.checkState(),
'ebokBox': GUI.ebokBox.checkState(),
'invertDirectionBox': GUI.invertDirectionBox.checkState(),
'languageEdit': GUI.languageEdit.text(),
@@ -1356,6 +1378,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"MOBI/AZW3": {'icon': 'MOBI', 'format': 'MOBI'},
"EPUB": {'icon': 'EPUB', 'format': 'EPUB'},
"CBZ": {'icon': 'CBZ', 'format': 'CBZ'},
"Folder of images": {'icon': 'FOLDER', 'format': 'FOLDER'},
"PDF": {'icon': 'EPUB', 'format': 'PDF'},
"PDF (200MB limit)": {'icon': 'EPUB', 'format': 'PDF-200MB'},
"KFX (Send to Kindle EPUB)": {'icon': 'KFX', 'format': 'KFX'},
@@ -1571,6 +1594,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.webtoonBox.stateChanged.connect(self.togglewebtoonBox)
GUI.qualityBox.stateChanged.connect(self.togglequalityBox)
GUI.mozJpegBox.stateChanged.connect(self.toggleImageFormatBox)
GUI.wallpaperBox.stateChanged.connect(self.toggleWallpaperBox)
GUI.chunkSizeCheckBox.stateChanged.connect(self.togglechunkSizeCheckBox)
GUI.deviceBox.activated.connect(self.changeDevice)
GUI.formatBox.activated.connect(self.changeFormat)
+12 -1
View File
@@ -352,6 +352,11 @@ class Ui_mainWindow(object):
self.gridLayout_2.addWidget(self.keepComicInfoBox, 11, 3, 1, 1)
self.wallpaperBox = QCheckBox(self.optionWidget)
self.wallpaperBox.setObjectName(u"wallpaperBox")
self.gridLayout_2.addWidget(self.wallpaperBox, 11, 0, 1, 1)
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
@@ -854,6 +859,12 @@ class Ui_mainWindow(object):
"Keeping this file may crash some readers like the Kobo native CBZ reader.", None))
#endif // QT_CONFIG(tooltip)
self.keepComicInfoBox.setText(QCoreApplication.translate("mainWindow", u"Keep ComicInfo.xml", None))
#if QT_CONFIG(tooltip)
self.wallpaperBox.setToolTip(QCoreApplication.translate("mainWindow", u"Auto check various options intended for KOreader wallpapers.\n"
"\n"
"Will also crop images to fill the screen centered.", None))
#endif // QT_CONFIG(tooltip)
self.wallpaperBox.setText(QCoreApplication.translate("mainWindow", u"Wallpaper mode", None))
#if QT_CONFIG(tooltip)
self.editorButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to edit directory.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
@@ -861,7 +872,7 @@ class Ui_mainWindow(object):
self.humbleButton.setText(QCoreApplication.translate("mainWindow", u"Humble Bundle Referral", None))
self.kofiButton.setText(QCoreApplication.translate("mainWindow", u"Support me on Ko-fi", None))
#if QT_CONFIG(tooltip)
self.labelSpreadsButton.setToolTip(QCoreApplication.translate("mainWindow", u"Hold shift while clicking for a higher quality preview.", None))
self.labelSpreadsButton.setToolTip(QCoreApplication.translate("mainWindow", u"Hold shift while clicking for a low quality preview.", None))
#endif // QT_CONFIG(tooltip)
self.labelSpreadsButton.setText(QCoreApplication.translate("mainWindow", u"Label Spreads", None))
#if QT_CONFIG(tooltip)
+1 -1
View File
@@ -1,4 +1,4 @@
__version__ = '11.0.2'
__version__ = '11.0.3'
__license__ = 'ISC'
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
__docformat__ = 'restructuredtext en'
+40 -6
View File
@@ -1058,6 +1058,8 @@ def getWorkFolder(afile, options, workdir=None):
def getOutputFilename(srcpath, wantedname, ext, tomenumber):
if options.folder_output:
ext = ''
source_path = Path(srcpath)
if srcpath[-1] == os.path.sep:
srcpath = srcpath[:-1]
@@ -1069,7 +1071,7 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
ext = '.kepub.epub'
if wantedname is not None:
wanted_root, wanted_ext = os.path.splitext(wantedname)
if wantedname.endswith(ext):
if not options.folder_output and wantedname.endswith(ext):
filename = os.path.abspath(wantedname)
elif wanted_ext == '.mobi' and ext == '.epub':
filename = os.path.abspath(wanted_root + ext)
@@ -1093,10 +1095,10 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
filename = source_path.with_name(name)
else:
filename = os.path.splitext(srcpath)[0] + tomenumber + ext
if os.path.isfile(filename):
if os.path.exists(filename):
counter = 0
basename = os.path.splitext(filename)[0]
while os.path.isfile(basename + '_kcc' + str(counter) + ext):
while os.path.exists(basename + '_kcc' + str(counter) + ext):
counter += 1
filename = basename + '_kcc' + str(counter) + ext
elif options.format == 'MOBI' and ext == '.epub':
@@ -1430,6 +1432,9 @@ def slugify(value, options, is_natural_sorted):
return value
def makeZIP(zipfilename, basedir, job_progress='', isepub=False):
if options.folder_output:
move(basedir, zipfilename)
return zipfilename
start = perf_counter()
if SEVENZIP in available_archive_tools():
if isepub:
@@ -1475,6 +1480,8 @@ def makeParser():
help="Manga style (right-to-left reading and splitting)")
main_options.add_argument("--lightnovel", action="store_true", dest="lightnovel", default=False,
help="Only resize images and preserve original file structure.")
main_options.add_argument("--wallpaper", action="store_true", dest="wallpaper", default=False,
help="Crop to fill screen.")
main_options.add_argument("--ebok", action="store_true", dest="ebok", default=False,
help="Force EBOK tag instead of PDOC for MOBI")
main_options.add_argument("--invertdirection", action="store_true", dest="invertdirection", default=False,
@@ -1639,6 +1646,12 @@ def checkOptions(options):
options.keep_epub = True
options.format = 'MOBI'
options.kfx = False
options.skip_zip = False
options.folder_output = False
if options.format == 'FOLDER':
options.format = 'CBZ'
options.skip_zip = True
options.folder_output = True
if options.format == 'Auto':
if options.profile in ['KDX']:
options.format = 'CBZ'
@@ -1811,6 +1824,7 @@ def makeBook(source, qtgui=None, job_progress=''):
print(f"{job_progress}Preparing source images...")
path = getWorkFolder(source, options)
print(f"{job_progress}Checking images...")
_, ext = os.path.splitext(source)
if options.lightnovel:
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
@@ -1831,7 +1845,6 @@ def makeBook(source, qtgui=None, job_progress=''):
if img.size[0] > x or img.size[1] > y:
img = ImageOps.contain(img, (x, y))
img.save(os.path.join(root, file), quality=options.jpegquality)
_, ext = os.path.splitext(source)
if ext != '.epub':
ext = '.cbz'
output_file = getOutputFilename(source, options.output, ext, '')
@@ -1859,6 +1872,8 @@ def makeBook(source, qtgui=None, job_progress=''):
if i in data['spreads']:
im1 = Image.open(os.path.join(root, sorted_files[i]))
im2 = Image.open(os.path.join(root, sorted_files[i+1]))
if not options.righttoleft:
im1, im2 = im2, im1
dst = Image.new('RGB', (im1.width + im2.width, im1.height))
dst.paste(im2, (0, 0))
dst.paste(im1, (im1.width, 0))
@@ -1872,6 +1887,25 @@ def makeBook(source, qtgui=None, job_progress=''):
if options.filefusion:
# Strip the fusion_0001_ sort prefix from makeFusion if present
chapterNames = {k: sub(r'^fusion_\d{4}_', '', v) for k, v in chapterNames.items()}
source_path = Path(source)
options.customcover = False
if source_path.parent.joinpath('Covers').is_dir():
series = os_sorted(filter(lambda s: s.endswith(ext) and '_kcc' not in s, os.listdir(source_path.parent)))
source_index = series.index(os.path.basename(source))
covers = os.listdir(source_path.parent.joinpath('Covers'))
filtered_covers = []
for cover in covers:
_, cover_ext = getImageFileName(cover)
if cover_ext in IMAGE_TYPES:
filtered_covers.append(cover)
sorted_covers = os_sorted(filtered_covers)
try:
cover_path = source_path.parent.joinpath('Covers', sorted_covers[source_index])
options.customcover = True
except IndexError:
pass
cover = None
if not options.webtoon:
cover = image.Cover(cover_path, options)
@@ -1953,7 +1987,7 @@ def makeBook(source, qtgui=None, job_progress=''):
filepath.append(getOutputFilename(source, options.output, '.cbz', ' ' + str(tomeNumber)))
else:
filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
if cover and cover.smartcover:
if cover and (cover.smartcover or options.customcover):
cover.save_to_folder(os.path.join(tome, 'OEBPS', 'Images', '##cover.jpg'), tomeNumber, len(tomes))
if options.comicinfo_xml:
with open(os.path.join(tome, 'OEBPS', 'Images', 'ComicInfo.xml'), 'wb') as xmlOutput:
@@ -1964,7 +1998,7 @@ def makeBook(source, qtgui=None, job_progress=''):
# determine output filename based on source and tome count
suffix = (' ' + str(tomeNumber)) if len(tomes) > 1 else ''
output_file = getOutputFilename(source, options.output, '.pdf', suffix)
if cover and cover.smartcover:
if cover and (cover.smartcover or options.customcover):
cover.save_to_folder(os.path.join(tome, 'OEBPS', 'Images', 'cover.jpg'), tomeNumber, len(tomes))
# use optimized buildPDF logic with streaming and compression
output_pdf = buildPDF(tome, options.title, job_progress, None, output_file)
+5 -1
View File
@@ -513,7 +513,9 @@ class ComicPage:
self.image = erase_rainbow_artifacts(self.image, is_color)
def resizeImage(self):
if self.opt.norotate and self.targetPathOrder in ('-kcc-a', '-kcc-d') and not self.opt.kindle_scribe_azw3:
if self.opt.wallpaper:
pass
elif self.opt.norotate and self.targetPathOrder in ('-kcc-a', '-kcc-d') and not self.opt.kindle_scribe_azw3:
# TODO: Kindle Scribe case
if self.opt.kindle_azw3 and any(dim > 1920 for dim in self.image.size):
self.image = ImageOps.contain(self.image, (1920, 1920), Image.Resampling.LANCZOS)
@@ -536,6 +538,8 @@ class ComicPage:
self.image = ImageOps.pad(self.image, self.opt.kfx_resolution, method=method, color=self.fill)
elif self.opt.stretch:
self.image = self.image.resize(self.size, method)
elif self.opt.wallpaper:
self.image = ImageOps.fit(self.image, self.size, method=method)
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
pass
else: # if image bigger than device resolution or smaller with upscaling