Merge branch 'master' into metadata-editor-in-bulk
This commit is contained in:
18 files changed
+163
-93
No files matched your search
@@ -38,7 +38,6 @@ from xml.sax.saxutils import escape
|
||||
from psutil import Process
|
||||
from copy import copy
|
||||
from packaging.version import Version
|
||||
from raven import Client
|
||||
from tempfile import gettempdir
|
||||
|
||||
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
|
||||
@@ -327,8 +326,8 @@ class WorkerThread(QThread):
|
||||
options.maximizestrips = True
|
||||
if GUI.disableProcessingBox.isChecked():
|
||||
options.noprocessing = True
|
||||
if GUI.pdfExtractBox.isChecked():
|
||||
options.pdfextract = True
|
||||
if GUI.legacyExtractBox.isChecked():
|
||||
options.legacyextract = True
|
||||
if GUI.pdfWidthBox.isChecked():
|
||||
options.pdfwidth = True
|
||||
if GUI.smartCoverCropBox.isChecked():
|
||||
@@ -345,6 +344,8 @@ class WorkerThread(QThread):
|
||||
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:
|
||||
@@ -445,8 +446,6 @@ class WorkerThread(QThread):
|
||||
_, _, traceback = sys.exc_info()
|
||||
MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s"
|
||||
% (jobargv[-1], str(err), sanitizeTrace(traceback)), 'error')
|
||||
if ' is corrupted.' not in str(err):
|
||||
GUI.sentry.captureException()
|
||||
MW.addMessage.emit('Error during conversion! Please consult '
|
||||
'<a href="https://github.com/ciromattia/kcc/wiki/Error-messages">wiki</a> '
|
||||
'for more details.', 'error', False)
|
||||
@@ -564,6 +563,7 @@ class WorkerThread(QThread):
|
||||
os.remove(path)
|
||||
elif os.path.isdir(path):
|
||||
rmtree(path, True)
|
||||
comic2ebook.checkPre('LLL-')
|
||||
GUI.progress.content = ''
|
||||
GUI.progress.stop()
|
||||
MW.hideProgressBar.emit()
|
||||
@@ -627,7 +627,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
GUI.jobList.clear()
|
||||
if self.tar or self.sevenzip:
|
||||
fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
|
||||
'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.pdf);;All (*.*)')
|
||||
'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.epub *.pdf);;All (*.*)')
|
||||
else:
|
||||
fnames = QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
|
||||
'Comic (*.pdf);;All (*.*)')
|
||||
@@ -699,7 +699,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
self.editor.loadData(files)
|
||||
except Exception as err:
|
||||
_, _, traceback = sys.exc_info()
|
||||
GUI.sentry.captureException()
|
||||
self.showDialog("Failed to parse metadata!\n\n%s\n\nTraceback:\n%s"
|
||||
% (str(err), sanitizeTrace(traceback)), 'error')
|
||||
else:
|
||||
@@ -1101,7 +1100,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
'colorBox': GUI.colorBox.checkState(),
|
||||
'eraseRainbowBox': GUI.eraseRainbowBox.checkState(),
|
||||
'disableProcessingBox': GUI.disableProcessingBox.checkState(),
|
||||
'pdfExtractBox': GUI.pdfExtractBox.checkState(),
|
||||
'legacyExtractBox': GUI.legacyExtractBox.checkState(),
|
||||
'pdfWidthBox': GUI.pdfWidthBox.checkState(),
|
||||
'smartCoverCropBox': GUI.smartCoverCropBox.checkState(),
|
||||
'coverFillBox': GUI.coverFillBox.checkState(),
|
||||
@@ -1118,6 +1117,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
'deleteBox': GUI.deleteBox.checkState(),
|
||||
'tempDirBox': GUI.tempDirBox.checkState(),
|
||||
'spreadShiftBox': GUI.spreadShiftBox.checkState(),
|
||||
'onePageLandscapeBox': GUI.onePageLandscapeBox.checkState(),
|
||||
'fileFusionBox': GUI.fileFusionBox.checkState(),
|
||||
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(),
|
||||
'noRotateBox': GUI.noRotateBox.checkState(),
|
||||
@@ -1141,7 +1141,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
GUI.jobList.clear()
|
||||
formats = ['.pdf']
|
||||
if self.tar or self.sevenzip:
|
||||
formats.extend(['.cb7', '.7z', '.cbz', '.zip', '.cbr', '.rar'])
|
||||
formats.extend(['.cb7', '.7z', '.cbz', '.zip', '.cbr', '.rar', '.epub'])
|
||||
if os.path.isdir(message):
|
||||
GUI.jobList.addItem(message)
|
||||
GUI.jobList.scrollToBottom()
|
||||
@@ -1228,7 +1228,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
self.croppingPowerValue = 1.0
|
||||
self.currentMode = 1
|
||||
self.targetDirectory = ''
|
||||
self.sentry = Client(release=__version__)
|
||||
if sys.platform.startswith('win'):
|
||||
# noinspection PyUnresolvedReferences
|
||||
from psutil import BELOW_NORMAL_PRIORITY_CLASS
|
||||
@@ -1775,7 +1774,6 @@ class KCCGUI_MetaEditor(KCC_ui_editor.Ui_editorDialog):
|
||||
self.parser.saveXML()
|
||||
except Exception as err:
|
||||
_, _, traceback = sys.exc_info()
|
||||
GUI.sentry.captureException()
|
||||
GUI.showDialog("Failed to save metadata!\n\n%s\n\nTraceback:\n%s"
|
||||
% (str(err), sanitizeTrace(traceback)), 'error')
|
||||
self.ui.close()
|
||||
|
||||
@@ -389,10 +389,10 @@ class Ui_mainWindow(object):
|
||||
|
||||
self.gridLayout_2.addWidget(self.qualityBox, 1, 2, 1, 1)
|
||||
|
||||
self.pdfExtractBox = QCheckBox(self.optionWidget)
|
||||
self.pdfExtractBox.setObjectName(u"pdfExtractBox")
|
||||
self.legacyExtractBox = QCheckBox(self.optionWidget)
|
||||
self.legacyExtractBox.setObjectName(u"legacyExtractBox")
|
||||
|
||||
self.gridLayout_2.addWidget(self.pdfExtractBox, 9, 0, 1, 1)
|
||||
self.gridLayout_2.addWidget(self.legacyExtractBox, 9, 0, 1, 1)
|
||||
|
||||
self.colorBox = QCheckBox(self.optionWidget)
|
||||
self.colorBox.setObjectName(u"colorBox")
|
||||
@@ -453,7 +453,12 @@ class Ui_mainWindow(object):
|
||||
self.tempDirBox = QCheckBox(self.optionWidget)
|
||||
self.tempDirBox.setObjectName(u"tempDirBox")
|
||||
|
||||
self.gridLayout_2.addWidget(self.tempDirBox, 12, 1, 1, 1)
|
||||
self.gridLayout_2.addWidget(self.tempDirBox, 12, 2, 1, 1)
|
||||
|
||||
self.onePageLandscapeBox = QCheckBox(self.optionWidget)
|
||||
self.onePageLandscapeBox.setObjectName(u"onePageLandscapeBox")
|
||||
|
||||
self.gridLayout_2.addWidget(self.onePageLandscapeBox, 12, 1, 1, 1)
|
||||
|
||||
|
||||
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
|
||||
@@ -785,11 +790,9 @@ class Ui_mainWindow(object):
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.qualityBox.setText(QCoreApplication.translate("mainWindow", u"Panel View 4/2/HQ", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.pdfExtractBox.setToolTip(QCoreApplication.translate("mainWindow", u"Use the PDF image extraction method from KCC 8 and earlier.\n"
|
||||
"\n"
|
||||
"Useful for really weird PDFs.", None))
|
||||
self.legacyExtractBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Use the PDF/EPUB image extraction method from older KCC versions.</p><p><br/></p><p>Use if standard extraction fails for whatever reason.</p></body></html>", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.pdfExtractBox.setText(QCoreApplication.translate("mainWindow", u"PDF Legacy Extract", None))
|
||||
self.legacyExtractBox.setText(QCoreApplication.translate("mainWindow", u"Legacy Extract", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.colorBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Disable conversion to grayscale.</p></body></html>", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
@@ -830,6 +833,10 @@ class Ui_mainWindow(object):
|
||||
self.tempDirBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Main Drive<br/></span>Use dedicated temporary directory on main OS drive.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Source File Drive<br/></span>Create temporary file directory on source file drive.</p></body></html>", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.tempDirBox.setText(QCoreApplication.translate("mainWindow", u"Temp Directory", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.onePageLandscapeBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 2 page landscape<br/></span>2 viewports for left and right pages</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 1 page landscape<br/></span>A single centered viewport for 1 page</p></body></html>", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.onePageLandscapeBox.setText(QCoreApplication.translate("mainWindow", u"1 Page Landscape", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.convertButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to select the output directory for this list.</p></body></html>", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
|
||||
@@ -22,7 +22,9 @@ from collections import Counter
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from argparse import ArgumentParser
|
||||
from time import perf_counter, strftime, gmtime
|
||||
from copy import copy
|
||||
@@ -30,8 +32,8 @@ from glob import glob, escape
|
||||
from re import sub
|
||||
from stat import S_IWRITE, S_IREAD, S_IEXEC
|
||||
from typing import List
|
||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from tempfile import mkdtemp, gettempdir, TemporaryFile
|
||||
from zipfile import ZipFile, ZIP_STORED
|
||||
from tempfile import mkdtemp, gettempdir
|
||||
from shutil import move, copytree, rmtree, copyfile
|
||||
from multiprocessing import Pool, cpu_count
|
||||
from uuid import uuid4
|
||||
@@ -90,6 +92,7 @@ def main(argv=None):
|
||||
os.remove(path)
|
||||
elif os.path.isdir(path):
|
||||
rmtree(path, True)
|
||||
checkPre('LLL-')
|
||||
return 0
|
||||
|
||||
|
||||
@@ -466,6 +469,8 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
|
||||
pageside = "right"
|
||||
|
||||
for entry, prop in zip(reflist, page_spread_property_list):
|
||||
if options.onepagelandscape:
|
||||
prop = 'center'
|
||||
f.write(f'<itemref idref="page_{entry}" {pageSpreadProperty(prop)}/>\n')
|
||||
|
||||
f.write("</spine>\n</package>\n")
|
||||
@@ -892,9 +897,12 @@ def getWorkFolder(afile, workdir=None):
|
||||
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
|
||||
else:
|
||||
fullPath = workdir
|
||||
check_path = gettempdir()
|
||||
|
||||
if options.tempdir:
|
||||
check_path = os.path.dirname(afile)
|
||||
else:
|
||||
check_path = gettempdir()
|
||||
|
||||
if os.path.isdir(afile):
|
||||
if disk_usage(check_path)[2] < getDirectorySize(afile) * 2.5:
|
||||
raise UserWarning("Not enough disk space to perform conversion.")
|
||||
@@ -913,7 +921,7 @@ def getWorkFolder(afile, workdir=None):
|
||||
os.makedirs(fullPath)
|
||||
path = workdir
|
||||
sanitizePermissions(path)
|
||||
if options.pdfextract:
|
||||
if options.legacyextract:
|
||||
pdf = pdfjpgextract.PdfJpgExtract(afile, fullPath)
|
||||
njpg = pdf.extract()
|
||||
if njpg == 0:
|
||||
@@ -952,11 +960,68 @@ def getWorkFolder(afile, workdir=None):
|
||||
for file in os.listdir(os.path.join(fullPath, tdir[0])):
|
||||
move(os.path.join(fullPath, tdir[0], file), fullPath)
|
||||
os.rmdir(os.path.join(fullPath, tdir[0]))
|
||||
|
||||
if options.legacyextract:
|
||||
return workdir
|
||||
|
||||
if afile.lower().endswith('.epub'):
|
||||
container = ET.parse(os.path.join(path, 'META-INF', 'container.xml'))
|
||||
opf_path = container.find(r'.//{*}rootfile').attrib['full-path']
|
||||
opf_path = os.path.join(path, opf_path)
|
||||
opf = ET.parse(opf_path)
|
||||
spine = []
|
||||
for spine_item in opf.findall(r'.//{*}itemref'):
|
||||
spine.append(spine_item.attrib.get('idref'))
|
||||
manifest_dict = {}
|
||||
for manifest_item in opf.findall(".//*[@media-type='application/xhtml+xml']"):
|
||||
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:
|
||||
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
|
||||
img_path = None
|
||||
for img in imgs:
|
||||
for key in img.attrib:
|
||||
if 'src' in key or 'href' in key:
|
||||
temp_img_path = img.attrib[key]
|
||||
if temp_img_path.startswith('..'):
|
||||
temp_img_path = os.path.join(os.path.dirname(opf_path), os.path.dirname(manifest_dict[spine_item]), temp_img_path)
|
||||
else:
|
||||
temp_img_path = os.path.join(os.path.dirname(opf_path), os.path.dirname(manifest_dict[spine_item]), temp_img_path)
|
||||
try:
|
||||
temp_size = os.path.getsize(temp_img_path)
|
||||
if temp_size > largest_size:
|
||||
largest_size = temp_size
|
||||
img_path = temp_img_path
|
||||
except OSError:
|
||||
pass
|
||||
# TODO empty image
|
||||
if img_path:
|
||||
ordered_image_paths.append(img_path)
|
||||
# fallback if naive spine extraction fails
|
||||
if not ordered_image_paths:
|
||||
return workdir
|
||||
|
||||
if options.tempdir:
|
||||
workdir2 = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
||||
else:
|
||||
workdir2 = mkdtemp('', 'KCC-')
|
||||
for i, img_path in enumerate(ordered_image_paths):
|
||||
_, ext = os.path.splitext(img_path)
|
||||
fullpath2 = os.path.join(workdir2, 'OEBPS', 'Images')
|
||||
os.makedirs(fullpath2, exist_ok=True)
|
||||
shutil.copyfile(img_path, os.path.join(fullpath2, f"{i}{ext}"))
|
||||
rmtree(workdir, True)
|
||||
return workdir2
|
||||
|
||||
return workdir
|
||||
|
||||
except OSError as e:
|
||||
rmtree(workdir, True)
|
||||
raise UserWarning(e)
|
||||
finally:
|
||||
pass
|
||||
else:
|
||||
raise UserWarning("Failed to open source file/directory.")
|
||||
|
||||
@@ -1393,6 +1458,8 @@ def makeParser():
|
||||
"2: Consider every subdirectory as separate volume [Default=0]")
|
||||
output_options.add_argument("--spreadshift", action="store_true", dest="spreadshift", default=False,
|
||||
help="Shift first page to opposite side in landscape for spread alignment")
|
||||
output_options.add_argument("--onepagelandscape", action="store_true", dest="onepagelandscape", default=False,
|
||||
help="Show a single centered page in landscape")
|
||||
output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False,
|
||||
help="Do not rotate double page spreads in spread splitter option.")
|
||||
output_options.add_argument("--rotateright", action="store_true", dest="rotateright", default=False,
|
||||
@@ -1402,8 +1469,8 @@ def makeParser():
|
||||
|
||||
processing_options.add_argument("-n", "--noprocessing", action="store_true", dest="noprocessing", default=False,
|
||||
help="Do not modify image and ignore any profile or processing option")
|
||||
processing_options.add_argument("--pdfextract", action="store_true", dest="pdfextract", default=False,
|
||||
help="Use the legacy PDF image extraction method from KCC 8 and earlier")
|
||||
processing_options.add_argument("--legacyextract", action="store_true", dest="legacyextract", default=False,
|
||||
help="Use the legacy PDF/EPUB image extraction method from older KCC versions")
|
||||
processing_options.add_argument("--pdfwidth", action="store_true", dest="pdfwidth", default=False,
|
||||
help="Render vector PDFs to device width instead of height.")
|
||||
processing_options.add_argument("--smartcovercrop", action="store_true", dest="smartcovercrop", default=False,
|
||||
@@ -1609,33 +1676,30 @@ def checkTools(source):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def checkPre(source):
|
||||
def checkPre(source='KCC-'):
|
||||
# Make sure that all temporary files are gone
|
||||
for root, dirs, _ in walkLevel(gettempdir(), 0):
|
||||
for tempdir in dirs:
|
||||
if tempdir.startswith('KCC-'):
|
||||
if tempdir.startswith(source):
|
||||
rmtree(os.path.join(root, tempdir), True)
|
||||
# Make sure that target directory is writable
|
||||
if os.path.isdir(source):
|
||||
src = os.path.abspath(os.path.join(source, '..'))
|
||||
else:
|
||||
src = os.path.dirname(source)
|
||||
try:
|
||||
with TemporaryFile(prefix='KCC-', dir=src):
|
||||
pass
|
||||
except Exception:
|
||||
raise UserWarning("Target directory is not writable.")
|
||||
|
||||
|
||||
def makeFusion(sources: List[str]):
|
||||
if len(sources) < 2:
|
||||
raise UserWarning('Fusion requires at least 2 sources. Did you forget to uncheck fusion?')
|
||||
start = perf_counter()
|
||||
first_path = Path(sources[0])
|
||||
if first_path.is_file():
|
||||
fusion_path = first_path.parent.joinpath(first_path.stem + ' [fused]')
|
||||
|
||||
if options.tempdir:
|
||||
fusion_parent = first_path.parent
|
||||
else:
|
||||
fusion_path = first_path.parent.joinpath(first_path.name + ' [fused]')
|
||||
# LLL is after KCC
|
||||
checkPre('LLL-')
|
||||
fusion_parent = Path(mkdtemp('', 'LLL-'))
|
||||
|
||||
if first_path.is_file():
|
||||
fusion_path = fusion_parent.joinpath(first_path.stem + ' [fused]')
|
||||
else:
|
||||
fusion_path = fusion_parent.joinpath(first_path.name + ' [fused]')
|
||||
print("Running Fusion")
|
||||
|
||||
# Check if prefix is needed when user-specified ordering differs from OS natural sorting
|
||||
@@ -1644,7 +1708,6 @@ def makeFusion(sources: List[str]):
|
||||
|
||||
for index, source in enumerate(sources, start=1):
|
||||
print(f"Processing {source}...")
|
||||
checkPre(source)
|
||||
print("Checking images...")
|
||||
source_path = Path(source)
|
||||
# Add the fusion_0001_ prefix to maintain user-specified order if needed
|
||||
@@ -1676,7 +1739,9 @@ def makeBook(source, qtgui=None, job_progress=''):
|
||||
GUI.progressBarTick.emit('1')
|
||||
else:
|
||||
checkTools(source)
|
||||
checkPre(source)
|
||||
checkPre()
|
||||
if not options.filefusion:
|
||||
checkPre('LLL-')
|
||||
print(f"{job_progress}Preparing source images...")
|
||||
path = getWorkFolder(source)
|
||||
print(f"{job_progress}Checking images...")
|
||||
|
||||
@@ -122,10 +122,6 @@ def dependencyCheck(level):
|
||||
missing.append('PySide 6.0.0')
|
||||
except ImportError:
|
||||
missing.append('PySide 6.0.0+')
|
||||
try:
|
||||
import raven
|
||||
except ImportError:
|
||||
missing.append('raven 6.0.0+')
|
||||
if level > 1:
|
||||
try:
|
||||
from psutil import __version__ as psutilVersion
|
||||
|
||||
Reference in new issue
Block a user