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