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

Compare commits

..

1 Commits

Author SHA1 Message Date
Alex Xu 3f19a08aa4 Revert "Bulk Metadata Editing" 2026-05-18 19:32:05 -07:00
10 changed files with 99 additions and 382 deletions
-2
View File
@@ -365,7 +365,6 @@ 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
```
@@ -391,7 +390,6 @@ 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,19 +62,6 @@
<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">
@@ -208,7 +195,6 @@
<tabstops>
<tabstop>seriesLine</tabstop>
<tabstop>volumeLine</tabstop>
<tabstop>bulkVolumeCheck</tabstop>
<tabstop>titleLine</tabstop>
<tabstop>numberLine</tabstop>
<tabstop>writerLine</tabstop>
+58 -310
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, QAbstractItemView, QListView, QTreeView)
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QTreeView, QAbstractItemView)
from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
import os
@@ -393,10 +393,7 @@ 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)
@@ -560,6 +557,13 @@ 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()
@@ -654,45 +658,27 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
def selectFileMetaEditor(self, sname):
files = []
if not sname:
if QApplication.keyboardModifiers() == Qt.ShiftModifier:
# 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])
dname = QFileDialog.getExistingDirectory(MW, 'Select directory', self.lastPath)
if dname != '':
sname = os.path.join(dname, 'ComicInfo.xml')
self.lastPath = os.path.dirname(sname)
else:
if self.sevenzip:
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))
fname = QFileDialog.getOpenFileName(MW, 'Select file', self.lastPath,
'Comic (*.cbz *.cbr *.cb7)')
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')
else:
files = [sname]
if files:
if fname[0] != '':
sname = fname[0]
self.lastPath = os.path.abspath(os.path.join(sname, os.pardir))
if sname:
try:
self.editor.loadData(files)
self.editor.loadData(sname)
except Exception as err:
_, _, traceback = sys.exc_info()
self.showDialog("Failed to parse metadata!\n\n%s\n\nTraceback:\n%s"
@@ -1550,299 +1536,61 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
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>'
)
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.')
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):
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 = [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.')
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(f'Successfully updated {total} files.')
self.ui.close()
self.statusLabel.setText(field.objectName().capitalize()[:-4] + ' field must be a number.')
break
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()
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'):
+4 -15
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, QCheckBox, QDialog, QGridLayout,
QHBoxLayout, QLabel, QLineEdit, QPushButton,
QSizePolicy, QVBoxLayout, QWidget)
from PySide6.QtWidgets import (QApplication, QDialog, QGridLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QSizePolicy,
QVBoxLayout, QWidget)
from . import KCC_rc
class Ui_editorDialog(object):
@@ -57,12 +57,6 @@ 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")
@@ -163,8 +157,7 @@ class Ui_editorDialog(object):
self.verticalLayout.addWidget(self.optionWidget)
QWidget.setTabOrder(self.seriesLine, self.volumeLine)
QWidget.setTabOrder(self.volumeLine, self.bulkVolumeCheck)
QWidget.setTabOrder(self.bulkVolumeCheck, self.titleLine)
QWidget.setTabOrder(self.volumeLine, self.titleLine)
QWidget.setTabOrder(self.titleLine, self.numberLine)
QWidget.setTabOrder(self.numberLine, self.writerLine)
QWidget.setTabOrder(self.writerLine, self.pencillerLine)
@@ -182,10 +175,6 @@ 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.2.0'
__version__ = '10.1.3'
__license__ = 'ISC'
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
__docformat__ = 'restructuredtext en'
+18 -17
View File
@@ -76,19 +76,23 @@ 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
@@ -899,11 +903,9 @@ 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(DISK_WARNING)
raise UserWarning("Not enough disk space to perform conversion.")
try:
copytree(afile, fullPath)
sanitizePermissions(fullPath)
@@ -913,7 +915,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(DISK_WARNING)
raise UserWarning("Not enough disk space to perform conversion.")
if afile.lower().endswith('.pdf'):
if not os.path.exists(fullPath):
os.makedirs(fullPath)
@@ -975,11 +977,10 @@ 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):
try:
page_path = os.path.join(os.path.dirname(opf_path), manifest_dict[spine_item])
page = ET.parse(page_path)
except Exception:
if spine_item not in manifest_dict:
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
@@ -1687,7 +1688,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:
@@ -1896,11 +1897,11 @@ def makeBook(source, qtgui=None, job_progress=''):
end = perf_counter()
print(f"{job_progress}makeBook: {end - start} seconds")
if options.filefusion:
rmtree(source, True)
checkPre('LLL-')
# Clean up temporary workspace
try:
rmtree(path, True)
except Exception:
pass
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 * 8:
if targetHeight > 131072 * 4:
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
+15 -20
View File
@@ -165,10 +165,7 @@ class ComicPageParser:
with Image.open(srcImgPath) as im:
self.image = im.copy()
self.page_background_color = self.fillCheck()
self.fill = self.page_background_color
if self.opt.bordersColor:
self.fill = self.opt.bordersColor
self.fill = self.fillCheck()
# backwards compatibility for Pillow >9.1.0
if not hasattr(Image, 'Resampling'):
Image.Resampling = Image
@@ -198,9 +195,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.page_background_color, self.fill])
self.payload.append(['N', self.source, new_image, self.fill])
elif self.opt.webtoon:
self.payload.append(['N', self.source, self.image, self.page_background_color, self.fill])
self.payload.append(['N', self.source, self.image, 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
@@ -209,12 +206,11 @@ 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.page_background_color, self.fill])
self.payload.append(['R', self.source, spread, self.fill])
# elif wide enough to split
elif (width > height) != (dstwidth > dstheight) and width / height > 1.16:
# if (split) or (split and rotate)
BISECT_THRESHOLD = 1.8
if self.opt.splitter != 1 and width / height < BISECT_THRESHOLD:
if self.opt.splitter != 1 and width / height < 1.75:
if width > height:
leftbox = (0, 0, int(width / 2), height)
rightbox = (int(width / 2), 0, width, height)
@@ -227,23 +223,23 @@ class ComicPageParser:
else:
pageone = self.image.crop(leftbox)
pagetwo = self.image.crop(rightbox)
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])
self.payload.append(['S1', self.source, pageone, self.fill])
self.payload.append(['S2', self.source, pagetwo, self.fill])
# if (rotate) or (split and rotate)
if self.opt.splitter > 0 or (self.opt.splitter == 0 and width / height >= BISECT_THRESHOLD):
if self.opt.splitter > 0 or (self.opt.splitter == 0 and width / height >= 1.75):
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.page_background_color, self.fill])
self.payload.append(['R', self.source, spread, self.fill])
else:
self.payload.append(['N', self.source, self.image, self.page_background_color, self.fill])
self.payload.append(['N', self.source, self.image, self.fill])
def fillCheck(self):
if False:
if self.opt.bordersColor:
return self.opt.bordersColor
else:
bw = self.image.convert('L').point(lambda x: 0 if x < 128 else 255, '1')
@@ -282,7 +278,7 @@ class ComicPageParser:
class ComicPage:
def __init__(self, options, mode, path, image, page_background_color, fill):
def __init__(self, options, mode, path, image, fill):
self.opt = options
_, self.size, self.palette, self.gamma = self.opt.profileData
if self.opt.hq:
@@ -292,7 +288,6 @@ 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])
@@ -566,7 +561,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.page_background_color)
bbox = get_bbox_crop_margin_page_number(self.image, power, self.fill)
if bbox:
w, h = self.image.size
@@ -576,7 +571,7 @@ class ComicPage:
self.maybeCrop(bbox, minimum)
def cropMargin(self, power, minimum):
bbox = get_bbox_crop_margin(self.image, power, self.page_background_color)
bbox = get_bbox_crop_margin(self.image, power, self.fill)
if bbox:
w, h = self.image.size
@@ -586,7 +581,7 @@ class ComicPage:
self.maybeCrop(bbox, minimum)
def cropInterPanelEmptySections(self, direction):
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.page_background_color)
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.fill)
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.25.5
PyMuPDF>=1.26.1
+1 -1
View File
@@ -1,7 +1,7 @@
PySide6==6.1.3
Pillow>=9
psutil>=5.9.5
requests>=2.32.4
requests>=2.34.2
python-slugify>=8.0.4
packaging>=26.2
mozjpeg-lossless-optimization>=1.2.0