1
0
mirror of https://github.com/ciromattia/kcc synced 2026-06-10 08:30:31 +00:00

Compare commits

..

14 Commits

Author SHA1 Message Date
Alex Xu 5e68ce380c double webtoon max height 2026-05-31 18:42:31 -07:00
Alex Xu b95cf6e179 ignore exceptions when epub parsing (#1360)
* ignore exceptions when epub parsing

* Update comic2ebook.py

* Update comic2ebook.py
2026-05-26 09:13:22 -07:00
フィルターペーパー 08070cdd97 Fix fusion output location (#1355)
* Fix fusion output location

Use source folder when output folder is not specified

* undelete things
2026-05-25 16:41:36 -07:00
Alex Xu df3d174437 fusion temp file cleanup improvements (#1359) 2026-05-25 14:52:08 -07:00
Alex Xu dc4475bcb0 bump to 10.2.0 2026-05-22 15:15:02 -07:00
Alex Xu b2e7fd3f5a fix macOS 10.14 PyMuPDF (#1354) 2026-05-22 15:02:13 -07:00
Alex Xu 4102643110 disk usage warning reccomends using temp directory option (#1353) 2026-05-22 11:55:42 -07:00
Alex Xu ca6b3b7611 fix file fusion when output folder unchecked (beta regression) (#1352) 2026-05-22 11:41:47 -07:00
Alex Xu 6d5db71b5b add pip install --upgrade pip to README.md 2026-05-21 21:07:57 -07:00
Alex Xu 87c6b7143a fix cropping with border options (#1351) 2026-05-21 13:28:12 -07:00
Alex Xu a5bd995a6b remove sentry 2026-05-20 07:55:58 -07:00
Alex Xu bcc69b0f05 Update requirements-win7.txt 2026-05-19 08:21:56 -07:00
Jarosław Janas f96adc5dc3 Metadata editor in bulk v2 (#1349)
* Add bulk metadata editing support

* Enable multi-file selection in Metadata Editor using getOpenFileNames()

* Disable Volume, Number, and Title fields in bulk mode

* Pre-fill Series and author fields from first selected file in bulk mode

* Show status "Editing Y files" in bulk mode

* Add progress display during bulk save ("Processing X/Y: filename")

* Disable buttons during processing

* Skip read-only CBR files with error collection

* Show error summary dialog if any files fail to save

* Keep single file metadata editing the same

* Add bulk volume editing with rich tooltip

* Add checkbox next to Volume field for enabling bulk volume editing (only in bulk mode)

* Support single number (5), range (1-10), or comma list (1, 3, 5) input formats

* Sort files alphabetically before volume assignment

* Validate that volume count matches file count

* Add rich HTML tooltip explaining the feature

* Move bulkVolumeCheck widget definition to MetaEditor.ui

* Define bulkVolumeCheck QCheckBox in MetaEditor.ui instead of creating it programmatically in KCC_gui.py

* Update tab order to include bulkVolumeCheck after volumeLine

* Add multi-directory selection for bulk metadata editing

* Shift+click on editor button now opens multi-directory selection dialog
* Multiple directories enable bulk mode with volume number assignment
* Uses Qt's built-in dialog with multi-selection enabled for native look and feel

* Fix volume input validation in bulk metadata editor

* Handle invalid range formats like "1-2-3" or "--" that previously fell through, now returns an error message

* Add explicit check for empty/malformed range parts before attempting to parse

* Add positive number validation for all input types (range, comma-list, single number) to be consistent with single file mode which uses isnumeric()

* Add explicit validation for empty volumes list after parsing

* Keep metadata editor dialog open when error occur during bulks save

* Fix CBR read-only check in bulk metadata editor

* Check all files for CBR format during load instead of during save

* Unify the CBR check for single and bulk mode

* Handle mixed metadata values in bulk editing

* In bulk mode, compare Series + Writer/Penciller/Inker/Colorist across all selected files (instead of using only the first file)

* When values differ, show “(multiple values)” placeholder for that field

* Add hover tooltip with overwrite warning and a File|Value table (or Value|Count summary when there are >20 files)

* Code comments cleanup

* Use ExtendedSelection for multi-directory file dialog

* use !=
2026-05-19 08:17:13 -07:00
Alex Xu f54b06e058 increase bisect threhold aspect ratio from 1.75 to 1.80 (#1348) 2026-05-18 21:42:44 -07:00
10 changed files with 382 additions and 99 deletions
+2
View File
@@ -365,6 +365,7 @@ One time setup and running for the first time:
```
python -m venv venv
venv\Scripts\activate.bat
pip install --upgrade pip
pip install -r requirements.txt
python kcc.py
```
@@ -390,6 +391,7 @@ One time setup and running for the first time:
```
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
python kcc.py
```
+14
View File
@@ -62,6 +62,19 @@
<item row="1" column="1">
<widget class="QLineEdit" name="volumeLine"/>
</item>
<item row="1" column="2">
<widget class="QCheckBox" name="bulkVolumeCheck">
<property name="visible">
<bool>false</bool>
</property>
<property name="toolTip">
<string>&lt;b&gt;Bulk Volume Editing&lt;/b&gt;&lt;br&gt;Check this box to assign volume numbers to multiple files.&lt;br&gt;&lt;br&gt;&lt;b&gt;Input formats:&lt;/b&gt;&lt;br&gt;&lt;code&gt;5&lt;/code&gt; → sequence starting from 5 (5, 6, 7...)&lt;br&gt;&lt;code&gt;1-10&lt;/code&gt; → range from 1 to 10&lt;br&gt;&lt;code&gt;1, 3, 5&lt;/code&gt; → specific values&lt;br&gt;&lt;br&gt;&lt;i&gt;Note: Files are sorted alphabetically before assignment.&lt;/i&gt;</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
@@ -195,6 +208,7 @@
<tabstops>
<tabstop>seriesLine</tabstop>
<tabstop>volumeLine</tabstop>
<tabstop>bulkVolumeCheck</tabstop>
<tabstop>titleLine</tabstop>
<tabstop>numberLine</tabstop>
<tabstop>writerLine</tabstop>
+310 -58
View File
@@ -22,7 +22,7 @@ import itertools
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, QTreeView, QAbstractItemView)
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QAbstractItemView, QListView, QTreeView)
from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
import os
@@ -393,7 +393,10 @@ class WorkerThread(QThread):
for job in currentJobs:
bookDir.append(job)
try:
fusion_source_parent = str(Path(bookDir[0]).parent)
comic2ebook.options = comic2ebook.checkOptions(copy(options))
if options.output is None:
options.output = fusion_source_parent
currentJobs.clear()
currentJobs.append(comic2ebook.makeFusion(bookDir))
MW.addMessage.emit('Created fusion at ' + currentJobs[0], 'info', False)
@@ -557,13 +560,6 @@ class WorkerThread(QThread):
move(item, GUI.targetDirectory)
except Exception:
pass
if options.filefusion:
for path in currentJobs:
if os.path.isfile(path):
os.remove(path)
elif os.path.isdir(path):
rmtree(path, True)
comic2ebook.checkPre('LLL-')
GUI.progress.content = ''
GUI.progress.stop()
MW.hideProgressBar.emit()
@@ -658,27 +654,45 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
def selectFileMetaEditor(self, sname):
files = []
if not sname:
if QApplication.keyboardModifiers() == Qt.ShiftModifier:
dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
if dname != '':
sname = os.path.join(dname, 'ComicInfo.xml')
self.lastPath = os.path.dirname(sname)
# Multi-directory selection for bulk editing ComicInfo.xml
dialog = QFileDialog(MW, 'Select volume directories', self.lastPath)
dialog.setFileMode(QFileDialog.FileMode.Directory)
dialog.setOption(QFileDialog.Option.ShowDirsOnly, True)
dialog.setOption(QFileDialog.Option.DontUseNativeDialog, True)
# Enable multi-selection in the dialog (may not work with native dialog on all platforms)
file_view = dialog.findChild(QListView, 'listView')
if file_view:
file_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
file_tree = dialog.findChild(QTreeView)
if file_tree:
file_tree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
if dialog.exec():
selected_dirs = dialog.selectedFiles()
if selected_dirs:
files = [os.path.join(d, 'ComicInfo.xml') for d in selected_dirs]
self.lastPath = os.path.dirname(selected_dirs[0])
else:
if self.sevenzip:
fname = QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath,
'Comic (*.cbz *.cbr *.cb7)')
fnames = QFileDialog.getOpenFileNames(MW, 'Select file(s)', self.lastPath,
'Comic (*.cbz *.cbr *.cb7)')
files = fnames[0]
if files:
self.lastPath = os.path.abspath(os.path.join(files[0], os.pardir))
else:
fname = ['']
self.showDialog("Editor is disabled due to a lack of 7z.", 'error')
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
' to enable metadata editing.', 'warning')
if fname[0] != '':
sname = fname[0]
self.lastPath = os.path.abspath(os.path.join(sname, os.pardir))
if sname:
else:
files = [sname]
if files:
try:
self.editor.loadData(sname)
self.editor.loadData(files)
except Exception as err:
_, _, traceback = sys.exc_info()
self.showDialog("Failed to parse metadata!\n\n%s\n\nTraceback:\n%s"
@@ -1536,61 +1550,299 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
def loadData(self, file):
self.parser = metadata.MetadataParser(file)
if self.parser.format in ['RAR', 'RAR5']:
self.editorWidget.setEnabled(False)
self.okButton.setEnabled(False)
self.statusLabel.setText('CBR metadata are read-only.')
def _buildBulkFieldToolTip(self, fieldLabel, valuesByFile):
note = '<p><em>Note: Changing this field will overwrite all values in all selected files.</em></p>'
if len(valuesByFile) <= 20:
rows = ''.join(
'<tr>'
f'<td style="padding:2px 6px; white-space:nowrap;">{escape(os.path.basename(f))}</td>'
f'<td style="padding:2px 6px;">{escape(v)}</td>'
'</tr>'
for f, v in valuesByFile
)
table = (
'<table border="1" cellspacing="0" cellpadding="0">'
'<tr>'
'<th style="padding:2px 6px; text-align:left;">File</th>'
'<th style="padding:2px 6px; text-align:left;">Value</th>'
'</tr>'
f'{rows}'
'</table>'
)
else:
counts = {}
for _, v in valuesByFile:
counts[v] = counts.get(v, 0) + 1
rows = ''.join(
'<tr>'
f'<td style="padding:2px 6px;">{escape(v)}</td>'
f'<td style="padding:2px 6px; text-align:right;">{c}</td>'
'</tr>'
for v, c in sorted(counts.items(), key=lambda t: (-t[1], t[0]))
)
table = (
'<table border="1" cellspacing="0" cellpadding="0">'
'<tr>'
'<th style="padding:2px 6px; text-align:left;">Value</th>'
'<th style="padding:2px 6px; text-align:right;">Count</th>'
'</tr>'
f'{rows}'
'</table>'
)
tooltipHTML = f'\
<b>{escape(fieldLabel)}</b>\
{note}\
{table}\
'
return tooltipHTML
def loadData(self, files):
self.files = files if isinstance(files, list) else [files]
self.bulkMode = len(self.files) > 1
# Sort files by name for consistent volume assignment
self.files.sort()
# Unified CBR check for all files (both single and bulk mode)
for file in self.files:
parser = metadata.MetadataParser(file)
if parser.format in ['RAR', 'RAR5']:
self.editorWidget.setEnabled(False)
self.okButton.setEnabled(False)
self.statusLabel.setText('CBR files in selection are read-only.')
return
if self.bulkMode:
firstFile = self.files[0]
self.parser = metadata.MetadataParser(firstFile)
self.editorWidget.setEnabled(True)
self.okButton.setEnabled(True)
self.statusLabel.setText(f'Editing {len(self.files)} files.')
# Show bulk volume checkbox
self.bulkVolumeCheck.setVisible(True)
self.bulkVolumeCheck.setChecked(False)
for field in (self.volumeLine, self.numberLine, self.titleLine):
field.setEnabled(False)
field.setText('')
field.setPlaceholderText('(multiple files)')
field.setToolTip('')
# Load metadata for all files and show common values, or “(multiple values)” + tooltip.
parsed = []
for file in self.files:
parsed.append((file, metadata.MetadataParser(file)))
field_specs = [
(self.seriesLine, 'Series', lambda p: (p.data.get('Series', '') or '')),
(self.writerLine, 'Writer', lambda p: ', '.join(p.data.get('Writers', []) or [])),
(self.pencillerLine, 'Penciller', lambda p: ', '.join(p.data.get('Pencillers', []) or [])),
(self.inkerLine, 'Inker', lambda p: ', '.join(p.data.get('Inkers', []) or [])),
(self.coloristLine, 'Colorist', lambda p: ', '.join(p.data.get('Colorists', []) or [])),
]
for line, label, extractor in field_specs:
line.setEnabled(True)
valuesByFile = [(f, extractor(p)) for f, p in parsed]
uniqueValues = {v for _, v in valuesByFile}
if len(uniqueValues) == 1:
common_value = valuesByFile[0][1] if valuesByFile else ''
line.setPlaceholderText('')
line.setToolTip('')
line.setText(common_value)
else:
line.setText('')
line.setPlaceholderText('(multiple values)')
line.setToolTip(self._buildBulkFieldToolTip(label, valuesByFile))
else:
file = self.files[0]
self.parser = metadata.MetadataParser(file)
# Hide bulk volume checkbox in single file mode
self.bulkVolumeCheck.setVisible(False)
for field in (self.volumeLine, self.numberLine, self.titleLine, self.seriesLine,
self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
field.setEnabled(True)
field.setPlaceholderText('')
self.editorWidget.setEnabled(True)
self.okButton.setEnabled(True)
self.statusLabel.setText('Separate authors with a comma.')
for field in (self.seriesLine, self.volumeLine, self.numberLine, self.titleLine):
field.setText(self.parser.data[field.objectName().capitalize()[:-4]])
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's']))
for field in (self.seriesLine, self.titleLine):
if field.text() == '':
path = Path(file)
if file.endswith('.xml'):
field.setText(path.parent.name)
else:
field.setText(path.stem)
for field in (self.seriesLine, self.volumeLine, self.numberLine, self.titleLine):
field.setText(self.parser.data[field.objectName().capitalize()[:-4]])
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
field.setText(', '.join(self.parser.data[field.objectName().capitalize()[:-4] + 's']))
for field in (self.seriesLine, self.titleLine):
if field.text() == '':
path = Path(file)
if file.endswith('.xml'):
field.setText(path.parent.name)
else:
field.setText(path.stem)
def saveData(self):
for field in (self.volumeLine, self.numberLine):
if field.text().isnumeric() or self.cleanData(field.text()) == '':
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text())
else:
self.statusLabel.setText(field.objectName().capitalize()[:-4] + ' field must be a number.')
break
else:
for field in (self.seriesLine, self.titleLine):
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text())
if self.bulkMode:
bulkData = {}
if self.cleanData(self.seriesLine.text()):
bulkData['Series'] = self.cleanData(self.seriesLine.text())
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
fieldName = field.objectName().capitalize()[:-4] + 's'
values = self.cleanData(field.text()).split(',')
tmpData = []
for value in values:
if self.cleanData(value) != '':
tmpData.append(self.cleanData(value))
self.parser.data[field.objectName().capitalize()[:-4] + 's'] = tmpData
try:
self.parser.saveXML()
except Exception as err:
_, _, traceback = sys.exc_info()
GUI.showDialog("Failed to save metadata!\n\n%s\n\nTraceback:\n%s"
% (str(err), sanitizeTrace(traceback)), 'error')
self.ui.close()
tmpData = [self.cleanData(v) for v in values if self.cleanData(v)]
if tmpData:
bulkData[fieldName] = tmpData
# Handle bulk volume editing
volumes = None
if self.bulkVolumeCheck.isChecked():
volumeText = self.volumeLine.text()
volumes, error = self.parseVolumeInput(volumeText, len(self.files))
if error:
self.statusLabel.setText(error)
return
if not bulkData and volumes is None:
self.statusLabel.setText('No changes to apply.')
return
errors = []
total = len(self.files)
self.okButton.setEnabled(False)
self.cancelButton.setEnabled(False)
for i, file in enumerate(self.files, 1):
self.statusLabel.setText(f'Processing {i}/{total}: {os.path.basename(file)}')
QApplication.processEvents()
try:
parser = metadata.MetadataParser(file)
if parser.format in ['RAR', 'RAR5']:
errors.append(f'{os.path.basename(file)}: CBR is read-only')
continue
for key, value in bulkData.items():
parser.data[key] = value
# Set volume if bulk volume editing is enabled
if volumes is not None:
parser.data['Volume'] = str(volumes[i - 1])
parser.saveXML()
except Exception as err:
errors.append(f'{os.path.basename(file)}: {str(err)}')
self.okButton.setEnabled(True)
self.cancelButton.setEnabled(True)
if errors:
GUI.showDialog("Some files failed to save:\n\n" + "\n".join(errors[:10]) +
(f"\n...and {len(errors) - 10} more" if len(errors) > 10 else ""), 'error')
self.statusLabel.setText('Errors occurred.')
else:
self.statusLabel.setText(f'Successfully updated {total} files.')
self.ui.close()
else:
for field in (self.volumeLine, self.numberLine):
if field.text().isnumeric() or self.cleanData(field.text()) == '':
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text())
else:
self.statusLabel.setText(field.objectName().capitalize()[:-4] + ' field must be a number.')
break
else:
for field in (self.seriesLine, self.titleLine):
self.parser.data[field.objectName().capitalize()[:-4]] = self.cleanData(field.text())
for field in (self.writerLine, self.pencillerLine, self.inkerLine, self.coloristLine):
values = self.cleanData(field.text()).split(',')
tmpData = []
for value in values:
if self.cleanData(value) != '':
tmpData.append(self.cleanData(value))
self.parser.data[field.objectName().capitalize()[:-4] + 's'] = tmpData
try:
self.parser.saveXML()
except Exception as err:
_, _, traceback = sys.exc_info()
GUI.showDialog("Failed to save metadata!\n\n%s\n\nTraceback:\n%s"
% (str(err), sanitizeTrace(traceback)), 'error')
self.ui.close()
def cleanData(self, s):
return escape(s.strip())
def parseVolumeInput(self, text, fileCount):
text = text.strip()
if not text:
return None, None
volumes = []
# Check if it's a range (e.g., "5-10")
if '-' in text and ',' not in text:
parts = text.split('-')
if len(parts) != 2 or not parts[0].strip() or not parts[1].strip():
return None, 'Invalid range format (use start-end)'
try:
start = int(parts[0].strip())
end = int(parts[1].strip())
if start < 0 or end < 0:
return None, 'Volume numbers must be positive'
if start > end:
return None, 'Invalid range: start > end'
volumes = list(range(start, end + 1))
except ValueError:
return None, 'Invalid range format'
# Check if it's a comma-separated list (e.g., "1,3,5")
elif ',' in text:
try:
volumes = [int(v.strip()) for v in text.split(',') if v.strip()]
if any(v < 0 for v in volumes):
return None, 'Volume numbers must be positive'
except ValueError:
return None, 'Invalid list format'
# Single number - generate sequence starting from that number
else:
try:
start = int(text)
if start < 0:
return None, 'Volume number must be positive'
volumes = list(range(start, start + fileCount))
except ValueError:
return None, 'Invalid number'
# Validate count
if not volumes:
return None, 'No valid volume numbers parsed'
if len(volumes) != fileCount:
return None, f'Volume count ({len(volumes)}) != file count ({fileCount})'
return volumes, None
def toggleBulkVolume(self, checked):
self.volumeLine.setEnabled(checked)
if checked:
self.volumeLine.setText('')
self.volumeLine.setPlaceholderText('e.g., 5 or 1-10 or 1,3,5')
else:
self.volumeLine.setText('')
self.volumeLine.setPlaceholderText('(multiple files)')
def __init__(self):
self.ui = QDialog()
self.parser = None
self.files = []
self.bulkMode = False
self.setupUi(self.ui)
self.ui.setWindowFlags(self.ui.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
self.bulkVolumeCheck.stateChanged.connect(self.toggleBulkVolume)
self.okButton.clicked.connect(self.saveData)
self.cancelButton.clicked.connect(self.ui.close)
if sys.platform.startswith('linux'):
+15 -4
View File
@@ -15,9 +15,9 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QDialog, QGridLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QSizePolicy,
QVBoxLayout, QWidget)
from PySide6.QtWidgets import (QApplication, QCheckBox, QDialog, QGridLayout,
QHBoxLayout, QLabel, QLineEdit, QPushButton,
QSizePolicy, QVBoxLayout, QWidget)
from . import KCC_rc
class Ui_editorDialog(object):
@@ -57,6 +57,12 @@ class Ui_editorDialog(object):
self.gridLayout.addWidget(self.volumeLine, 1, 1, 1, 1)
self.bulkVolumeCheck = QCheckBox(self.editorWidget)
self.bulkVolumeCheck.setObjectName(u"bulkVolumeCheck")
self.bulkVolumeCheck.setVisible(False)
self.gridLayout.addWidget(self.bulkVolumeCheck, 1, 2, 1, 1)
self.label_3 = QLabel(self.editorWidget)
self.label_3.setObjectName(u"label_3")
@@ -157,7 +163,8 @@ class Ui_editorDialog(object):
self.verticalLayout.addWidget(self.optionWidget)
QWidget.setTabOrder(self.seriesLine, self.volumeLine)
QWidget.setTabOrder(self.volumeLine, self.titleLine)
QWidget.setTabOrder(self.volumeLine, self.bulkVolumeCheck)
QWidget.setTabOrder(self.bulkVolumeCheck, self.titleLine)
QWidget.setTabOrder(self.titleLine, self.numberLine)
QWidget.setTabOrder(self.numberLine, self.writerLine)
QWidget.setTabOrder(self.writerLine, self.pencillerLine)
@@ -175,6 +182,10 @@ class Ui_editorDialog(object):
editorDialog.setWindowTitle(QCoreApplication.translate("editorDialog", u"Metadata editor", None))
self.label_1.setText(QCoreApplication.translate("editorDialog", u"Series:", None))
self.label_2.setText(QCoreApplication.translate("editorDialog", u"Volume:", None))
#if QT_CONFIG(tooltip)
self.bulkVolumeCheck.setToolTip(QCoreApplication.translate("editorDialog", u"<b>Bulk Volume Editing</b><br>Check this box to assign volume numbers to multiple files.<br><br><b>Input formats:</b><br><code>5</code> \u2192 sequence starting from 5 (5, 6, 7...)<br><code>1-10</code> \u2192 range from 1 to 10<br><code>1, 3, 5</code> \u2192 specific values<br><br><i>Note: Files are sorted alphabetically before assignment.</i>", None))
#endif // QT_CONFIG(tooltip)
self.bulkVolumeCheck.setText("")
self.label_3.setText(QCoreApplication.translate("editorDialog", u"Number:", None))
self.label_4.setText(QCoreApplication.translate("editorDialog", u"Writer:", None))
self.label_5.setText(QCoreApplication.translate("editorDialog", u"Penciller:", None))
+1 -1
View File
@@ -1,4 +1,4 @@
__version__ = '10.1.3'
__version__ = '10.2.0'
__license__ = 'ISC'
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
__docformat__ = 'restructuredtext en'
+17 -18
View File
@@ -76,23 +76,19 @@ def main(argv=None):
print('No matching files found.')
return 1
if options.filefusion:
fusion_source_parent = str(Path(sources[0]).parent)
fusion_path = makeFusion(list(sources))
sources.clear()
sources.append(fusion_path)
for source in sources:
source = source.rstrip('\\').rstrip('/')
options = copy(args)
if options.filefusion and options.output is None:
options.output = fusion_source_parent
options = checkOptions(options)
print('Working on ' + source + '...')
makeBook(source)
if options.filefusion:
for path in sources:
if os.path.isfile(path):
os.remove(path)
elif os.path.isdir(path):
rmtree(path, True)
checkPre('LLL-')
return 0
@@ -903,9 +899,11 @@ def getWorkFolder(afile, workdir=None):
else:
check_path = gettempdir()
DISK_WARNING = "Not enough disk space to perform conversion. Try Temp Directory option."
if os.path.isdir(afile):
if disk_usage(check_path)[2] < getDirectorySize(afile) * 2.5:
raise UserWarning("Not enough disk space to perform conversion.")
raise UserWarning(DISK_WARNING)
try:
copytree(afile, fullPath)
sanitizePermissions(fullPath)
@@ -915,7 +913,7 @@ def getWorkFolder(afile, workdir=None):
raise UserWarning("Failed to prepare a workspace.")
elif os.path.isfile(afile):
if disk_usage(check_path)[2]< os.path.getsize(afile) * 2.5:
raise UserWarning("Not enough disk space to perform conversion.")
raise UserWarning(DISK_WARNING)
if afile.lower().endswith('.pdf'):
if not os.path.exists(fullPath):
os.makedirs(fullPath)
@@ -977,10 +975,11 @@ def getWorkFolder(afile, workdir=None):
manifest_dict[manifest_item.attrib.get('id')] = manifest_item.attrib.get('href')
ordered_image_paths = []
for i, spine_item in enumerate(spine):
if spine_item not in manifest_dict:
try:
page_path = os.path.join(os.path.dirname(opf_path), manifest_dict[spine_item])
page = ET.parse(page_path)
except Exception:
continue
page_path = os.path.join(os.path.dirname(opf_path), manifest_dict[spine_item])
page = ET.parse(page_path)
imgs = page.findall(r'.//{*}img') + page.findall(r'.//{*}image')
largest_size = 0
@@ -1688,7 +1687,7 @@ def makeFusion(sources: List[str]):
raise UserWarning('Fusion requires at least 2 sources. Did you forget to uncheck fusion?')
start = perf_counter()
first_path = Path(sources[0])
if options.tempdir:
fusion_parent = first_path.parent
else:
@@ -1897,11 +1896,11 @@ def makeBook(source, qtgui=None, job_progress=''):
end = perf_counter()
print(f"{job_progress}makeBook: {end - start} seconds")
# Clean up temporary workspace
try:
rmtree(path, True)
except Exception:
pass
if options.filefusion:
rmtree(source, True)
checkPre('LLL-')
return filepath
+1 -1
View File
@@ -62,7 +62,7 @@ def mergeDirectory(work):
imagesValid.append(i[0])
# Silently drop directories that contain too many images
# 131072 = GIMP_MAX_IMAGE_SIZE / 4
if targetHeight > 131072 * 4:
if targetHeight > 131072 * 8:
raise RuntimeError(f'Image too tall at {targetHeight} pixels. {targetWidth} pixels wide. Try using separate chapter folders or file fusion.')
result = Image.new('RGB', (targetWidth, targetHeight))
y = 0
+20 -15
View File
@@ -165,7 +165,10 @@ class ComicPageParser:
with Image.open(srcImgPath) as im:
self.image = im.copy()
self.fill = self.fillCheck()
self.page_background_color = self.fillCheck()
self.fill = self.page_background_color
if self.opt.bordersColor:
self.fill = self.opt.bordersColor
# backwards compatibility for Pillow >9.1.0
if not hasattr(Image, 'Resampling'):
Image.Resampling = Image
@@ -195,9 +198,9 @@ class ComicPageParser:
new_image = Image.new("RGB", (int(width / 2), int(height*2)))
new_image.paste(pageone, (0, 0))
new_image.paste(pagetwo, (0, height))
self.payload.append(['N', self.source, new_image, self.fill])
self.payload.append(['N', self.source, new_image, self.page_background_color, self.fill])
elif self.opt.webtoon:
self.payload.append(['N', self.source, self.image, self.fill])
self.payload.append(['N', self.source, self.image, self.page_background_color, self.fill])
# rotate only TODO dead code?
elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth and self.opt.splitter == 1:
spread = self.image
@@ -206,11 +209,12 @@ class ComicPageParser:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
else:
spread = spread.rotate(-90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread, self.fill])
self.payload.append(['R', self.source, spread, self.page_background_color, self.fill])
# elif wide enough to split
elif (width > height) != (dstwidth > dstheight) and width / height > 1.16:
# if (split) or (split and rotate)
if self.opt.splitter != 1 and width / height < 1.75:
BISECT_THRESHOLD = 1.8
if self.opt.splitter != 1 and width / height < BISECT_THRESHOLD:
if width > height:
leftbox = (0, 0, int(width / 2), height)
rightbox = (int(width / 2), 0, width, height)
@@ -223,23 +227,23 @@ class ComicPageParser:
else:
pageone = self.image.crop(leftbox)
pagetwo = self.image.crop(rightbox)
self.payload.append(['S1', self.source, pageone, self.fill])
self.payload.append(['S2', self.source, pagetwo, self.fill])
self.payload.append(['S1', self.source, pageone, self.page_background_color, self.fill])
self.payload.append(['S2', self.source, pagetwo, self.page_background_color, self.fill])
# if (rotate) or (split and rotate)
if self.opt.splitter > 0 or (self.opt.splitter == 0 and width / height >= 1.75):
if self.opt.splitter > 0 or (self.opt.splitter == 0 and width / height >= BISECT_THRESHOLD):
spread = self.image
if not self.opt.norotate:
if not self.opt.rotateright:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
else:
spread = spread.rotate(-90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread, self.fill])
self.payload.append(['R', self.source, spread, self.page_background_color, self.fill])
else:
self.payload.append(['N', self.source, self.image, self.fill])
self.payload.append(['N', self.source, self.image, self.page_background_color, self.fill])
def fillCheck(self):
if self.opt.bordersColor:
if False:
return self.opt.bordersColor
else:
bw = self.image.convert('L').point(lambda x: 0 if x < 128 else 255, '1')
@@ -278,7 +282,7 @@ class ComicPageParser:
class ComicPage:
def __init__(self, options, mode, path, image, fill):
def __init__(self, options, mode, path, image, page_background_color, fill):
self.opt = options
_, self.size, self.palette, self.gamma = self.opt.profileData
if self.opt.hq:
@@ -288,6 +292,7 @@ class ComicPage:
self.image = image.convert("RGB")
self.color = self.colorCheck()
self.colorOutput = self.color and self.opt.forcecolor
self.page_background_color = page_background_color
self.fill = fill
self.rotated = False
self.orgPath = os.path.join(path[0], path[1])
@@ -561,7 +566,7 @@ class ComicPage:
self.image = self.image.crop(box)
def cropPageNumber(self, power, minimum):
bbox = get_bbox_crop_margin_page_number(self.image, power, self.fill)
bbox = get_bbox_crop_margin_page_number(self.image, power, self.page_background_color)
if bbox:
w, h = self.image.size
@@ -571,7 +576,7 @@ class ComicPage:
self.maybeCrop(bbox, minimum)
def cropMargin(self, power, minimum):
bbox = get_bbox_crop_margin(self.image, power, self.fill)
bbox = get_bbox_crop_margin(self.image, power, self.page_background_color)
if bbox:
w, h = self.image.size
@@ -581,7 +586,7 @@ class ComicPage:
self.maybeCrop(bbox, minimum)
def cropInterPanelEmptySections(self, direction):
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.fill)
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.page_background_color)
class Cover:
def __init__(self, source, opt):
+1 -1
View File
@@ -8,4 +8,4 @@ mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0
numpy<2
PyMuPDF>=1.26.1
PyMuPDF==1.25.5
+1 -1
View File
@@ -1,7 +1,7 @@
PySide6==6.1.3
Pillow>=9
psutil>=5.9.5
requests>=2.34.2
requests>=2.32.4
python-slugify>=8.0.4
packaging>=26.2
mozjpeg-lossless-optimization>=1.2.0