From b3e1d393b66a1d705f8c745f962e4500a486fda8 Mon Sep 17 00:00:00 2001
From: Alex Xu
Date: Fri, 31 Jul 2026 22:25:41 -0700
Subject: [PATCH] Manually label spreads button (#1412)
* initial commit
* initial commit 2
* json it
* UI labels
* basics
* add todo
* options to getWorkFolder
* refactor options
* refactor UI
* cleanup
* pdf fixed size
* remove comment
* finalized json creation
* fix help text
* finalized
* remove print
* add youtube
---
gui/KCC.ui | 145 +++++-----
kindlecomicconverter/KCC_gui.py | 344 ++++++++++++++---------
kindlecomicconverter/KCC_spread_label.py | 93 ++++++
kindlecomicconverter/KCC_ui.py | 112 ++++----
kindlecomicconverter/comic2ebook.py | 32 ++-
5 files changed, 467 insertions(+), 259 deletions(-)
create mode 100644 kindlecomicconverter/KCC_spread_label.py
diff --git a/gui/KCC.ui b/gui/KCC.ui
index d6b1204..e0b0953 100644
--- a/gui/KCC.ui
+++ b/gui/KCC.ui
@@ -6,7 +6,7 @@
0
0
- 644
+ 649
706
@@ -711,7 +711,27 @@ Keeping this file may crash some readers like the Kobo native CBZ reader.
0
- -
+
-
+
+
+
+ 0
+ 30
+
+
+
+ <html><head/><body><p style='white-space:pre'>Shift+Click to edit directory.</p></body></html>
+
+
+ Metadata Editor
+
+
+
+ :/Other/icons/editor.png:/Other/icons/editor.png
+
+
+
+ -
@@ -728,7 +748,7 @@ Keeping this file may crash some readers like the Kobo native CBZ reader.
- -
+
-
@@ -751,23 +771,10 @@ Keeping this file may crash some readers like the Kobo native CBZ reader.
- -
-
-
-
- 0
- 30
-
-
-
- <html><head/><body><p style='white-space:pre'>Shift+Click to edit directory.</p></body></html>
-
+
-
+
- Metadata Editor
-
-
-
- :/Other/icons/editor.png:/Other/icons/editor.png
+ Label Spreads
@@ -1074,6 +1081,56 @@ Keeping this file may crash some readers like the Kobo native CBZ reader.
0
+ -
+
+
+
+ 0
+ 28
+
+
+
+ <html><head/><body><p style='white-space:pre'>Target device.</p></body></html>
+
+
+
+ -
+
+
+
+ 0
+ 30
+
+
+
+ Clear list
+
+
+
+ :/Other/icons/clear.png:/Other/icons/clear.png
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ <html><head/><body><p style='white-space:pre'>Add directory containing JPG, PNG or GIF files to queue.<br/><span style=" font-weight:600;">CBR, CBZ and CB7 files inside will not be processed!</span></p></body></html>
+
+
+ Add input folder(s)
+
+
+
+ :/Other/icons/folder_new.png:/Other/icons/folder_new.png
+
+
+
-
@@ -1099,36 +1156,6 @@ Keeping this file may crash some readers like the Kobo native CBZ reader.
- -
-
-
-
- 0
- 30
-
-
-
- Clear list
-
-
-
- :/Other/icons/clear.png:/Other/icons/clear.png
-
-
-
- -
-
-
-
- 0
- 28
-
-
-
- <html><head/><body><p style='white-space:pre'>Target device.</p></body></html>
-
-
-
-
@@ -1149,26 +1176,6 @@ Keeping this file may crash some readers like the Kobo native CBZ reader.
- -
-
-
-
- 0
- 0
-
-
-
- <html><head/><body><p style='white-space:pre'>Add directory containing JPG, PNG or GIF files to queue.<br/><span style=" font-weight:600;">CBR, CBZ and CB7 files inside will not be processed!</span></p></body></html>
-
-
- Add input folder(s)
-
-
-
- :/Other/icons/folder_new.png:/Other/icons/folder_new.png
-
-
-
-
diff --git a/kindlecomicconverter/KCC_gui.py b/kindlecomicconverter/KCC_gui.py
index f35ff75..a0381eb 100644
--- a/kindlecomicconverter/KCC_gui.py
+++ b/kindlecomicconverter/KCC_gui.py
@@ -19,10 +19,11 @@
from datetime import datetime, timezone
import itertools
+import json
from pathlib import Path
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
-from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices)
-from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QAbstractItemView, QListView, QTreeView)
+from PySide6.QtGui import (QColor, QIcon, QImage, QKeyEvent, QPixmap, QDesktopServices)
+from PySide6.QtWidgets import (QApplication, QDialogButtonBox, QHBoxLayout, QLabel, QListWidgetItem, QMainWindow, QSizePolicy, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QAbstractItemView, QListView, QTreeView, QWidget)
from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
import os
@@ -38,10 +39,15 @@ from xml.sax.saxutils import escape
from psutil import Process
from copy import copy
from packaging.version import Version
-from tempfile import gettempdir
+from tempfile import gettempdir, mkdtemp
+from PIL import Image
+from PIL.Image import Dither
+
+from .KCC_spread_label import LabelSpreadsDialog
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
from .comicarchive import SEVENZIP, TAR, available_archive_tools
+from .comic2ebook import OS_SORT_KEY, flattenTree, getWorkFolder, removeNonImages, sanitizeTree
from . import __version__
from . import comic2ebook
from . import metadata
@@ -234,6 +240,138 @@ class ProgressThread(QThread):
self.running = False
+def get_options():
+ parser = comic2ebook.makeParser()
+ options = parser.parse_args()
+
+ options.profile = GUI.profiles[str(GUI.deviceBox.currentText())]['Label']
+ gui_current_format = GUI.formats[str(GUI.formatBox.currentText())]['format']
+ options.format = gui_current_format
+ if GUI.mangaBox.isChecked():
+ options.righttoleft = True
+ if GUI.lightnovelBox.isChecked():
+ options.lightnovel = True
+ if GUI.ebokBox.isChecked():
+ options.ebok = True
+ if GUI.invertDirectionBox.isChecked():
+ options.invertdirection = True
+ if GUI.rotateBox.checkState() == Qt.CheckState.PartiallyChecked:
+ options.splitter = 2
+ elif GUI.rotateBox.checkState() == Qt.CheckState.Checked:
+ options.splitter = 1
+ if GUI.qualityBox.checkState() == Qt.CheckState.PartiallyChecked:
+ options.autoscale = True
+ elif GUI.qualityBox.checkState() == Qt.CheckState.Checked:
+ options.hq = True
+ if GUI.vertical4PanelBox.isChecked():
+ options.vertical4panel = True
+ if GUI.webtoonBox.isChecked():
+ options.webtoon = True
+ if GUI.upscaleBox.checkState() == Qt.CheckState.PartiallyChecked:
+ options.stretch = True
+ elif GUI.upscaleBox.checkState() == Qt.CheckState.Checked:
+ options.upscale = True
+ if GUI.gammaBox.isChecked() and float(GUI.gammaValue) > 0.09:
+ options.gamma = float(GUI.gammaValue)
+ if GUI.autoLevelBox.isChecked():
+ options.autolevel = True
+ if GUI.autocontrastBox.checkState() == Qt.CheckState.PartiallyChecked:
+ options.noautocontrast = True
+ elif GUI.autocontrastBox.checkState() == Qt.CheckState.Checked:
+ options.colorautocontrast = True
+ if GUI.croppingBox.isChecked():
+ if GUI.croppingBox.checkState() == Qt.CheckState.PartiallyChecked:
+ options.cropping = 1
+ else:
+ options.cropping = 2
+ else:
+ options.cropping = 0
+ if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked:
+ options.croppingp = float(GUI.croppingPowerValue)
+ options.preservemargin = GUI.preserveMarginBox.value()
+ if GUI.interPanelCropBox.isChecked():
+ if GUI.interPanelCropBox.checkState() == Qt.CheckState.PartiallyChecked:
+ options.interpanelcrop = 1
+ else:
+ options.interpanelcrop = 2
+ else:
+ options.interpanelcrop = 0
+ if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked:
+ options.white_borders = True
+ elif GUI.borderBox.checkState() == Qt.CheckState.Checked:
+ options.black_borders = True
+ if GUI.outputSplit.isChecked():
+ options.batchsplit = 2
+ if GUI.colorBox.isChecked():
+ options.forcecolor = True
+ if GUI.eraseRainbowBox.isChecked():
+ options.eraserainbow = True
+ if GUI.maximizeStrips.isChecked():
+ options.maximizestrips = True
+ if GUI.disableProcessingBox.isChecked():
+ options.noprocessing = True
+ if GUI.legacyExtractBox.isChecked():
+ options.legacyextract = True
+ if GUI.pdfWidthBox.isChecked():
+ options.pdfwidth = True
+ if GUI.smartCoverCropBox.isChecked():
+ options.smartcovercrop = True
+ if GUI.coverFillBox.isChecked():
+ options.coverfill = True
+ if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked:
+ options.metadatatitle = 1
+ elif GUI.metadataTitleBox.checkState() == Qt.CheckState.Checked:
+ options.metadatatitle = 2
+ if GUI.keepComicInfoBox.isChecked():
+ options.keepcomicinfo = True
+ if GUI.deleteBox.isChecked():
+ options.delete = True
+ if GUI.tempDirBox.isChecked():
+ options.tempdir = True
+ if GUI.spreadShiftBox.isChecked():
+ options.spreadshift = True
+ if GUI.onePageLandscapeBox.isChecked():
+ options.onepagelandscape = True
+ if GUI.fileFusionBox.isChecked():
+ options.filefusion = True
+ else:
+ options.filefusion = False
+ if GUI.noRotateBox.isChecked():
+ options.norotate = True
+ if GUI.rotateRightBox.isChecked():
+ options.rotateright = True
+ if GUI.rotateFirstBox.isChecked():
+ options.rotatefirst = True
+ if GUI.forcePngRgbBox.isChecked():
+ options.force_png_rgb = True
+ if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
+ options.forcepng = True
+ elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
+ options.mozjpeg = True
+ if GUI.webpBox.isChecked():
+ options.webp = True
+ if GUI.pngLegacyBox.isChecked():
+ options.pnglegacy = True
+ if GUI.noQuantizeBox.isChecked():
+ options.noquantize = True
+ if GUI.jpegQualityBox.isChecked():
+ options.jpegquality = GUI.jpegQualitySpinBox.value()
+ if GUI.currentMode > 2:
+ options.customwidth = str(GUI.widthBox.value())
+ options.customheight = str(GUI.heightBox.value())
+ if GUI.targetDirectory != '':
+ options.output = GUI.targetDirectory
+ if GUI.titleEdit.text():
+ options.title = str(GUI.titleEdit.text())
+ if GUI.authorEdit.text():
+ options.author = str(GUI.authorEdit.text())
+ if GUI.languageEdit.text():
+ options.language = str(GUI.languageEdit.text())
+ if GUI.chunkSizeCheckBox.isChecked():
+ options.targetsize = int(GUI.chunkSizeBox.value())
+
+ return options, gui_current_format
+
class WorkerThread(QThread):
def __init__(self):
QThread.__init__(self)
@@ -263,136 +401,9 @@ class WorkerThread(QThread):
def run(self):
MW.modeConvert.emit(0)
- parser = comic2ebook.makeParser()
- options = parser.parse_args()
argv = ''
currentJobs = []
-
- options.profile = GUI.profiles[str(GUI.deviceBox.currentText())]['Label']
- gui_current_format = GUI.formats[str(GUI.formatBox.currentText())]['format']
- options.format = gui_current_format
- if GUI.mangaBox.isChecked():
- options.righttoleft = True
- if GUI.lightnovelBox.isChecked():
- options.lightnovel = True
- if GUI.ebokBox.isChecked():
- options.ebok = True
- if GUI.invertDirectionBox.isChecked():
- options.invertdirection = True
- if GUI.rotateBox.checkState() == Qt.CheckState.PartiallyChecked:
- options.splitter = 2
- elif GUI.rotateBox.checkState() == Qt.CheckState.Checked:
- options.splitter = 1
- if GUI.qualityBox.checkState() == Qt.CheckState.PartiallyChecked:
- options.autoscale = True
- elif GUI.qualityBox.checkState() == Qt.CheckState.Checked:
- options.hq = True
- if GUI.vertical4PanelBox.isChecked():
- options.vertical4panel = True
- if GUI.webtoonBox.isChecked():
- options.webtoon = True
- if GUI.upscaleBox.checkState() == Qt.CheckState.PartiallyChecked:
- options.stretch = True
- elif GUI.upscaleBox.checkState() == Qt.CheckState.Checked:
- options.upscale = True
- if GUI.gammaBox.isChecked() and float(GUI.gammaValue) > 0.09:
- options.gamma = float(GUI.gammaValue)
- if GUI.autoLevelBox.isChecked():
- options.autolevel = True
- if GUI.autocontrastBox.checkState() == Qt.CheckState.PartiallyChecked:
- options.noautocontrast = True
- elif GUI.autocontrastBox.checkState() == Qt.CheckState.Checked:
- options.colorautocontrast = True
- if GUI.croppingBox.isChecked():
- if GUI.croppingBox.checkState() == Qt.CheckState.PartiallyChecked:
- options.cropping = 1
- else:
- options.cropping = 2
- else:
- options.cropping = 0
- if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked:
- options.croppingp = float(GUI.croppingPowerValue)
- options.preservemargin = GUI.preserveMarginBox.value()
- if GUI.interPanelCropBox.isChecked():
- if GUI.interPanelCropBox.checkState() == Qt.CheckState.PartiallyChecked:
- options.interpanelcrop = 1
- else:
- options.interpanelcrop = 2
- else:
- options.interpanelcrop = 0
- if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked:
- options.white_borders = True
- elif GUI.borderBox.checkState() == Qt.CheckState.Checked:
- options.black_borders = True
- if GUI.outputSplit.isChecked():
- options.batchsplit = 2
- if GUI.colorBox.isChecked():
- options.forcecolor = True
- if GUI.eraseRainbowBox.isChecked():
- options.eraserainbow = True
- if GUI.maximizeStrips.isChecked():
- options.maximizestrips = True
- if GUI.disableProcessingBox.isChecked():
- options.noprocessing = True
- if GUI.legacyExtractBox.isChecked():
- options.legacyextract = True
- if GUI.pdfWidthBox.isChecked():
- options.pdfwidth = True
- if GUI.smartCoverCropBox.isChecked():
- options.smartcovercrop = True
- if GUI.coverFillBox.isChecked():
- options.coverfill = True
- if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked:
- options.metadatatitle = 1
- elif GUI.metadataTitleBox.checkState() == Qt.CheckState.Checked:
- options.metadatatitle = 2
- if GUI.keepComicInfoBox.isChecked():
- options.keepcomicinfo = True
- if GUI.deleteBox.isChecked():
- options.delete = True
- if GUI.tempDirBox.isChecked():
- options.tempdir = True
- if GUI.spreadShiftBox.isChecked():
- options.spreadshift = True
- if GUI.onePageLandscapeBox.isChecked():
- options.onepagelandscape = True
- if GUI.fileFusionBox.isChecked():
- options.filefusion = True
- else:
- options.filefusion = False
- if GUI.noRotateBox.isChecked():
- options.norotate = True
- if GUI.rotateRightBox.isChecked():
- options.rotateright = True
- if GUI.rotateFirstBox.isChecked():
- options.rotatefirst = True
- if GUI.forcePngRgbBox.isChecked():
- options.force_png_rgb = True
- if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
- options.forcepng = True
- elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
- options.mozjpeg = True
- if GUI.webpBox.isChecked():
- options.webp = True
- if GUI.pngLegacyBox.isChecked():
- options.pnglegacy = True
- if GUI.noQuantizeBox.isChecked():
- options.noquantize = True
- if GUI.jpegQualityBox.isChecked():
- options.jpegquality = GUI.jpegQualitySpinBox.value()
- if GUI.currentMode > 2:
- options.customwidth = str(GUI.widthBox.value())
- options.customheight = str(GUI.heightBox.value())
- if GUI.targetDirectory != '':
- options.output = GUI.targetDirectory
- if GUI.titleEdit.text():
- options.title = str(GUI.titleEdit.text())
- if GUI.authorEdit.text():
- options.author = str(GUI.authorEdit.text())
- if GUI.languageEdit.text():
- options.language = str(GUI.languageEdit.text())
- if GUI.chunkSizeCheckBox.isChecked():
- options.targetsize = int(GUI.chunkSizeBox.value())
+ options, gui_current_format = get_options()
for i in range(GUI.jobList.count()):
# Make sure that we don't consider any system message as job to do
@@ -659,6 +670,75 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.jobList.addItem(dname)
GUI.jobList.scrollToBottom()
+ def labelSpreadsStart(self):
+ currentJobs = []
+ # TODO: make this a function since it's copy pasted
+ for i in range(GUI.jobList.count()):
+ # Make sure that we don't consider any system message as job to do
+ if GUI.jobList.item(i).icon().isNull():
+ currentJobs.append(str(GUI.jobList.item(i).text()))
+ for job in currentJobs:
+ images = []
+ spreads = []
+ options, _ = get_options()
+ options.profileData = [(600, 800), (600,800)]
+ path = getWorkFolder(job, options)
+ removeNonImages(path)
+ sanitizeTree(path, options)
+ flattenTree(path)
+
+ if options.tempdir:
+ workdir = mkdtemp('', 'KCC-', os.path.dirname(job))
+ else:
+ workdir = mkdtemp('', 'KCC-')
+
+ for root, _, files in os.walk(path):
+ files.sort(key=OS_SORT_KEY)
+ start_index = 0
+ if job.endswith('.pdf') or job.endswith('.epub'):
+ start_index = 1
+ if options.spreadshift:
+ start_index = 0 if start_index == 1 else 1
+ for i in range(start_index, len(files), 2):
+ if i == len(files) - 1:
+ continue
+ # TODO: with statements
+ # TODO: ignore 1% of top and bottom too?
+ im1 = Image.open(os.path.join(root, files[i])).convert('1', dither=Dither.NONE)
+ size1 = im1.size
+ crop1 = im1.crop((0, 0, 0.2*size1[0], size1[1]))
+ crop11 = im1.crop((0.01*size1[0], 0, 0.04*size1[0], size1[1]))
+ #crop1 = crop11
+ im2 = Image.open(os.path.join(root, files[i+1])).convert('1', dither=Dither.NONE)
+ size2 = im2.size
+ crop2 = im2.crop((0.8*size2[0], 0, size2[0], size2[1]))
+ crop22 = im2.crop((0.96*size2[0], 0, .99* size2[0], size2[1]))
+ #crop2 = crop22
+ # dst = Image.new('1', (im1.width + im2.width, im1.height))
+ # dst.paste(im2, (0, 0))
+ # dst.paste(im1, (im1.width, 0))
+ hist1 = crop11.histogram()
+ hist2 = crop22.histogram()
+ # TODO: small percentage instead of zero
+ if hist1[0] == 0 or hist1[-1] == 0 or hist2[0] == 0 or hist2[-1] == 0:
+ continue
+
+ dst = Image.new('1', (crop1.width + crop2.width, crop1.height))
+
+ dst.paste(crop2, (0, 0))
+ dst.paste(crop1, (crop1.width, 0))
+ dst.save(os.path.join(workdir, f'label-{i:04}.png'))
+ images.append(os.path.join(workdir, f'label-{i:04}.png'))
+
+ dlg = LabelSpreadsDialog(APP.primaryScreen().availableGeometry().height(), images, spreads)
+ dlg.setWindowTitle(job)
+ if dlg.exec() == 1:
+ with open(job+'.json', "w") as fp:
+ # TODO: not very clean to grab index from filename
+ spreads = [int(filename[6:10]) for filename in spreads]
+ json.dump({'spreads': sorted(set(spreads))} , fp)
+ rmtree(path, True)
+ rmtree(workdir, True)
def selectFileMetaEditor(self, sname):
files = []
@@ -1436,6 +1516,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'README': "https://github.com/ciromattia/kcc?tab=readme-ov-file#kcc",
'FAQ': "https://github.com/ciromattia/kcc/blob/master/README.md#faq",
'WIKI': "https://github.com/ciromattia/kcc/wiki",
+ 'YOUTUBE': "https://www.youtube.com/@eink-dude",
'TUTORIAL': "https://youtu.be/QQ6zJcMF2Iw?si=80rfm6DU6OUJdFqa",
'EMAIL': "https://github.com/ciromattia/kcc?tab=readme-ov-file#commissions",
'DONATE': "https://github.com/ciromattia/kcc/blob/master/README.md#issues--new-features--donations",
@@ -1472,6 +1553,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.kofiButton.clicked.connect(self.openKofi)
GUI.humbleButton.clicked.connect(self.openHumble)
GUI.convertButton.clicked.connect(self.convertStart)
+ GUI.labelSpreadsButton.clicked.connect(self.labelSpreadsStart)
GUI.gammaSlider.valueChanged.connect(self.changeGamma)
GUI.gammaBox.stateChanged.connect(self.togglegammaBox)
GUI.croppingBox.stateChanged.connect(self.togglecroppingBox)
diff --git a/kindlecomicconverter/KCC_spread_label.py b/kindlecomicconverter/KCC_spread_label.py
new file mode 100644
index 0000000..4e393b9
--- /dev/null
+++ b/kindlecomicconverter/KCC_spread_label.py
@@ -0,0 +1,93 @@
+import os
+
+from PySide6.QtCore import (Qt)
+from PySide6.QtGui import (QKeyEvent, QPixmap)
+from PySide6.QtWidgets import (QDialogButtonBox, QHBoxLayout, QLabel, QDialog)
+
+class LabelSpreadsDialog(QDialog):
+ def __init__(self, available_height, images, spreads):
+ super().__init__()
+ self.index = 0
+ self.images = images
+ self.spreads = spreads
+ self.index2page = {i: os.path.basename(image) for i, image in enumerate(images)}
+
+ self.setWindowTitle("TODO: Filename goes here")
+ # self.setGeometry(APP.primaryScreen().availableGeometry())
+ # self.setMaximumSize(APP.primaryScreen().availableSize())
+ self.available_height = available_height
+
+ QBtn = (
+ QDialogButtonBox.Yes | QDialogButtonBox.No
+ )
+
+ self.buttonBox = QDialogButtonBox(QBtn)
+ self.buttonBox.accepted.connect(self.accept)
+ self.buttonBox.rejected.connect(self.reject)
+
+ layout = QHBoxLayout()
+ self.setLayout(layout)
+
+ label = QLabel()
+ label2 = QLabel()
+ self.label = label
+ self.label2 = label2
+ layout.addWidget(label)
+ layout.addWidget(label2)
+ label2.setText("not a spread")
+
+ help_text = [
+ "Press Yes to save labels to file.",
+ "Use arrows to change index.",
+ "Use space bar to confirm spreads.",
+ "Use spread shift option to offset by 1.",
+ ]
+ buttonLabel = QLabel('\n'.join(help_text))
+ layout.addWidget(buttonLabel)
+ layout.addWidget(self.buttonBox)
+ # print(label.size())
+ # print(label.maximumSize())
+ # l, t, r, b = layout.getContentsMargins()
+
+ #label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ pixmap = QPixmap(images[0]).scaledToHeight(self.available_height * 0.9)
+ label.setPixmap(pixmap)
+ #label.setScaledContents(True)
+
+ #label2.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+ # pixmap2 = QPixmap(images[0]).scaledToHeight(self.frameGeometry().height() - t - b - t - b)
+ # label2.setPixmap(pixmap2)
+ #label2.setScaledContents(True)
+ #self.resize(pixmap2.width(), pixmap2.height())
+ def keyReleaseEvent(self, event):
+ # t = 20
+ # b = 20
+ if isinstance(event, QKeyEvent):
+ if event.key() == Qt.Key.Key_Left:
+ self.index = max(0, self.index - 1)
+ if self.index2page[self.index] in self.spreads:
+ self.label2.setText('spread')
+ else:
+ self.label2.setText('not a spread')
+ pixmap = QPixmap(self.images[self.index]).scaledToHeight(self.available_height * 0.9)
+ self.label.setPixmap(pixmap)
+ # pixmap2 = QPixmap(images[self.index]).scaledToHeight(self.frameGeometry().height() - t - b - t - b)
+ # self.label2.setPixmap(pixmap2)
+ elif event.key() == Qt.Key.Key_Right:
+ self.index = min(self.index + 1, len(self.images) - 1)
+ if self.index2page[self.index] in self.spreads:
+ self.label2.setText('spread')
+ else:
+ self.label2.setText('not a spread')
+
+ pixmap = QPixmap(self.images[self.index]).scaledToHeight(self.available_height * 0.9)
+ self.label.setPixmap(pixmap)
+ # pixmap2 = QPixmap(images[self.index]).scaledToHeight(self.frameGeometry().height() - t - b - t - b)
+ # self.label2.setPixmap(pixmap2)
+ elif event.key() == Qt.Key.Key_Space:
+ self.spreads.append(os.path.basename(self.images[self.index]))
+ self.label2.setText('spread')
+ else:
+ super().keyReleaseEvent(event)
+ else:
+ super().keyReleaseEvent(event)
\ No newline at end of file
diff --git a/kindlecomicconverter/KCC_ui.py b/kindlecomicconverter/KCC_ui.py
index 27c95f1..b47fa0d 100644
--- a/kindlecomicconverter/KCC_ui.py
+++ b/kindlecomicconverter/KCC_ui.py
@@ -26,7 +26,7 @@ class Ui_mainWindow(object):
def setupUi(self, mainWindow):
if not mainWindow.objectName():
mainWindow.setObjectName(u"mainWindow")
- mainWindow.resize(644, 706)
+ mainWindow.resize(649, 706)
icon = QIcon()
icon.addFile(u":/Icon/icons/comic2ebook.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
mainWindow.setWindowIcon(icon)
@@ -360,33 +360,38 @@ class Ui_mainWindow(object):
self.gridLayout_6 = QGridLayout(self.toolWidget)
self.gridLayout_6.setObjectName(u"gridLayout_6")
self.gridLayout_6.setContentsMargins(0, 0, 0, 0)
+ self.editorButton = QPushButton(self.toolWidget)
+ self.editorButton.setObjectName(u"editorButton")
+ self.editorButton.setMinimumSize(QSize(0, 30))
+ icon2 = QIcon()
+ icon2.addFile(u":/Other/icons/editor.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.editorButton.setIcon(icon2)
+
+ self.gridLayout_6.addWidget(self.editorButton, 0, 0, 1, 1)
+
self.humbleButton = QPushButton(self.toolWidget)
self.humbleButton.setObjectName(u"humbleButton")
self.humbleButton.setMinimumSize(QSize(0, 30))
- icon2 = QIcon()
- icon2.addFile(u":/Brand/icons/Humble_H-Red.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
- self.humbleButton.setIcon(icon2)
+ icon3 = QIcon()
+ icon3.addFile(u":/Brand/icons/Humble_H-Red.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.humbleButton.setIcon(icon3)
- self.gridLayout_6.addWidget(self.humbleButton, 0, 2, 1, 1)
+ self.gridLayout_6.addWidget(self.humbleButton, 0, 3, 1, 1)
self.kofiButton = QPushButton(self.toolWidget)
self.kofiButton.setObjectName(u"kofiButton")
self.kofiButton.setMinimumSize(QSize(0, 30))
- icon3 = QIcon()
- icon3.addFile(u":/Brand/icons/kofi_symbol.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
- self.kofiButton.setIcon(icon3)
+ icon4 = QIcon()
+ icon4.addFile(u":/Brand/icons/kofi_symbol.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.kofiButton.setIcon(icon4)
self.kofiButton.setIconSize(QSize(19, 16))
- self.gridLayout_6.addWidget(self.kofiButton, 0, 1, 1, 1)
+ self.gridLayout_6.addWidget(self.kofiButton, 0, 2, 1, 1)
- self.editorButton = QPushButton(self.toolWidget)
- self.editorButton.setObjectName(u"editorButton")
- self.editorButton.setMinimumSize(QSize(0, 30))
- icon4 = QIcon()
- icon4.addFile(u":/Other/icons/editor.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
- self.editorButton.setIcon(icon4)
+ self.labelSpreadsButton = QPushButton(self.toolWidget)
+ self.labelSpreadsButton.setObjectName(u"labelSpreadsButton")
- self.gridLayout_6.addWidget(self.editorButton, 0, 0, 1, 1)
+ self.gridLayout_6.addWidget(self.labelSpreadsButton, 0, 1, 1, 1)
self.gridLayout.addWidget(self.toolWidget, 0, 0, 1, 2)
@@ -543,39 +548,20 @@ class Ui_mainWindow(object):
self.gridLayout_4 = QGridLayout(self.buttonWidget)
self.gridLayout_4.setObjectName(u"gridLayout_4")
self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
- self.convertButton = QPushButton(self.buttonWidget)
- self.convertButton.setObjectName(u"convertButton")
- self.convertButton.setMinimumSize(QSize(0, 30))
- self.convertButton.setFont(font)
- icon5 = QIcon()
- icon5.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
- self.convertButton.setIcon(icon5)
-
- self.gridLayout_4.addWidget(self.convertButton, 1, 3, 1, 1)
-
- self.clearButton = QPushButton(self.buttonWidget)
- self.clearButton.setObjectName(u"clearButton")
- self.clearButton.setMinimumSize(QSize(0, 30))
- icon6 = QIcon()
- icon6.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
- self.clearButton.setIcon(icon6)
-
- self.gridLayout_4.addWidget(self.clearButton, 0, 3, 1, 1)
-
self.deviceBox = QComboBox(self.buttonWidget)
self.deviceBox.setObjectName(u"deviceBox")
self.deviceBox.setMinimumSize(QSize(0, 28))
self.gridLayout_4.addWidget(self.deviceBox, 1, 1, 1, 1)
- self.fileButton = QPushButton(self.buttonWidget)
- self.fileButton.setObjectName(u"fileButton")
- self.fileButton.setMinimumSize(QSize(0, 30))
- icon7 = QIcon()
- icon7.addFile(u":/Other/icons/document_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
- self.fileButton.setIcon(icon7)
+ self.clearButton = QPushButton(self.buttonWidget)
+ self.clearButton.setObjectName(u"clearButton")
+ self.clearButton.setMinimumSize(QSize(0, 30))
+ icon5 = QIcon()
+ icon5.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.clearButton.setIcon(icon5)
- self.gridLayout_4.addWidget(self.fileButton, 0, 1, 1, 1)
+ self.gridLayout_4.addWidget(self.clearButton, 0, 3, 1, 1)
self.directoryButton = QPushButton(self.buttonWidget)
self.directoryButton.setObjectName(u"directoryButton")
@@ -588,6 +574,25 @@ class Ui_mainWindow(object):
self.gridLayout_4.addWidget(self.directoryButton, 0, 4, 1, 1)
+ self.convertButton = QPushButton(self.buttonWidget)
+ self.convertButton.setObjectName(u"convertButton")
+ self.convertButton.setMinimumSize(QSize(0, 30))
+ self.convertButton.setFont(font)
+ icon6 = QIcon()
+ icon6.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.convertButton.setIcon(icon6)
+
+ self.gridLayout_4.addWidget(self.convertButton, 1, 3, 1, 1)
+
+ self.fileButton = QPushButton(self.buttonWidget)
+ self.fileButton.setObjectName(u"fileButton")
+ self.fileButton.setMinimumSize(QSize(0, 30))
+ icon7 = QIcon()
+ icon7.addFile(u":/Other/icons/document_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
+ self.fileButton.setIcon(icon7)
+
+ self.gridLayout_4.addWidget(self.fileButton, 0, 1, 1, 1)
+
self.formatBox = QComboBox(self.buttonWidget)
self.formatBox.setObjectName(u"formatBox")
self.formatBox.setMinimumSize(QSize(0, 28))
@@ -848,12 +853,13 @@ 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))
- 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.editorButton.setToolTip(QCoreApplication.translate("mainWindow", u"
Shift+Click to edit directory.
", None))
#endif // QT_CONFIG(tooltip)
self.editorButton.setText(QCoreApplication.translate("mainWindow", u"Metadata Editor", None))
+ self.humbleButton.setText(QCoreApplication.translate("mainWindow", u"Humble Bundle Referral", None))
+ self.kofiButton.setText(QCoreApplication.translate("mainWindow", u"Support me on Ko-fi", None))
+ self.labelSpreadsButton.setText(QCoreApplication.translate("mainWindow", u"Label Spreads", None))
#if QT_CONFIG(tooltip)
self.chunkSizeWidget.setToolTip(QCoreApplication.translate("mainWindow", u"Warning: chunk size greater than default may cause
performance/battery issues, especially on older devices.
", None))
#endif // QT_CONFIG(tooltip)
@@ -878,22 +884,22 @@ class Ui_mainWindow(object):
#if QT_CONFIG(tooltip)
self.heightBox.setToolTip(QCoreApplication.translate("mainWindow", u" Resolution of the target device.
", None))
#endif // QT_CONFIG(tooltip)
-#if QT_CONFIG(tooltip)
- self.convertButton.setToolTip(QCoreApplication.translate("mainWindow", u"Shift+Click to select the output directory for this list.
", None))
-#endif // QT_CONFIG(tooltip)
- self.convertButton.setText(QCoreApplication.translate("mainWindow", u"Convert", None))
- self.clearButton.setText(QCoreApplication.translate("mainWindow", u"Clear list", None))
#if QT_CONFIG(tooltip)
self.deviceBox.setToolTip(QCoreApplication.translate("mainWindow", u" Target device.
", None))
#endif // QT_CONFIG(tooltip)
-#if QT_CONFIG(tooltip)
- self.fileButton.setToolTip(QCoreApplication.translate("mainWindow", u" Add CBR, CBZ, CB7 or PDF file to queue.
", None))
-#endif // QT_CONFIG(tooltip)
- self.fileButton.setText(QCoreApplication.translate("mainWindow", u"Add input file(s)", None))
+ self.clearButton.setText(QCoreApplication.translate("mainWindow", u"Clear list", None))
#if QT_CONFIG(tooltip)
self.directoryButton.setToolTip(QCoreApplication.translate("mainWindow", u" Add directory containing JPG, PNG or GIF files to queue.
CBR, CBZ and CB7 files inside will not be processed!
", None))
#endif // QT_CONFIG(tooltip)
self.directoryButton.setText(QCoreApplication.translate("mainWindow", u"Add input folder(s)", None))
+#if QT_CONFIG(tooltip)
+ self.convertButton.setToolTip(QCoreApplication.translate("mainWindow", u"
Shift+Click to select the output directory for this list.
", None))
+#endif // QT_CONFIG(tooltip)
+ self.convertButton.setText(QCoreApplication.translate("mainWindow", u"Convert", None))
+#if QT_CONFIG(tooltip)
+ self.fileButton.setToolTip(QCoreApplication.translate("mainWindow", u"Add CBR, CBZ, CB7 or PDF file to queue.
", None))
+#endif // QT_CONFIG(tooltip)
+ self.fileButton.setText(QCoreApplication.translate("mainWindow", u"Add input file(s)", None))
#if QT_CONFIG(tooltip)
self.formatBox.setToolTip(QCoreApplication.translate("mainWindow", u"Output format.
", None))
#endif // QT_CONFIG(tooltip)
diff --git a/kindlecomicconverter/comic2ebook.py b/kindlecomicconverter/comic2ebook.py
index 8f30f70..5ee27ae 100755
--- a/kindlecomicconverter/comic2ebook.py
+++ b/kindlecomicconverter/comic2ebook.py
@@ -20,6 +20,7 @@
from collections import Counter
from datetime import datetime
+import json
import os
import pathlib
import re
@@ -872,7 +873,7 @@ def extract_page(vector):
-def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_height):
+def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_height, pdfwidth):
render = False
with pymupdf.open(filename) as doc:
for page in doc:
@@ -892,7 +893,7 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_
cpu = cpu_count()
# make vectors of arguments for the processes
- vectors = [(i, cpu, filename, output_dir, target_width, target_height, options.pdfwidth) for i in range(cpu)]
+ vectors = [(i, cpu, filename, output_dir, target_width, target_height, pdfwidth) for i in range(cpu)]
print("Starting %i processes for '%s'." % (cpu, filename))
@@ -955,7 +956,7 @@ def getWorkFolder(afile, options, workdir=None):
target_height *= 1.25 #Account for possible margin at the top and bottom with page number
target_width *= 1.25
try:
- mupdf_pdf_process_pages_parallel(afile, fullPath, target_width, target_height)
+ mupdf_pdf_process_pages_parallel(afile, fullPath, target_width, target_height, options.pdfwidth)
except Exception as e:
rmtree(path, True)
raise UserWarning(f"Failed to extract images from PDF file. {e}")
@@ -1229,7 +1230,7 @@ def removeNonImages(filetree):
raise RuntimeError('\n'.join(warning))
-def sanitizeTree(filetree, prefix='kcc'):
+def sanitizeTree(filetree, options, prefix='kcc'):
chapterNames = {}
page = 1
cover_path = None
@@ -1785,7 +1786,7 @@ def makeFusion(sources: List[str]):
path = getWorkFolder(source, options, str(targetpath))
if path != str(targetpath):
move(os.path.join(path, 'OEBPS', 'Images'), targetpath)
- sanitizeTree(targetpath, prefix='fusion')
+ sanitizeTree(targetpath, options, prefix='fusion')
# TODO: remove flattenTree when subchapters are supported
flattenTree(targetpath)
@@ -1842,7 +1843,26 @@ def makeBook(source, qtgui=None, job_progress=''):
getMetadata(os.path.join(path, "OEBPS", "Images"), source)
removeNonImages(os.path.join(path, "OEBPS", "Images"))
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
- chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
+ chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'), options)
+
+ if os.path.exists(source+'.json'):
+ flattenTree(os.path.join(path, 'OEBPS', 'Images'))
+ with open(source+'.json') as f:
+ data = json.load(f)
+ for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
+ sorted_files = os_sorted(files)
+ for i in range(len(files)):
+ 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]))
+ dst = Image.new('RGB', (im1.width + im2.width, im1.height))
+ dst.paste(im2, (0, 0))
+ dst.paste(im1, (im1.width, 0))
+ base, _ = os.path.splitext(os.path.basename(sorted_files[i]))
+ dst.save(os.path.join(path, 'OEBPS', 'Images', f'{base}-merged.png'))
+ os.remove(os.path.join(root, sorted_files[i]))
+ os.remove(os.path.join(root, sorted_files[i+1]))
+
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()}