diff --git a/README.md b/README.md index cfaf2145..3f8b6767 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,10 @@ Options: -f FORMAT, --format=FORMAT Output format (Available options: Auto, MOBI, EPUB, CBZ) [Default=Auto] - -b, --batchsplit Split output into multiple files + -b BATCHSPLIT, --batchsplit=BATCHSPLIT + Split output into multiple files. 0: Don't split 1: + Automatic mode 2: Consider every subdirectory as + separate volume [Default=0] PROCESSING: -u, --upscale Resize images smaller than device's resolution @@ -160,6 +163,11 @@ The app relies and includes the following scripts: * [Kobo Aura ONE](http://kcc.iosphe.re/Samples/Ubunchu-KoAO.kepub.epub) ## CHANGELOG +####5.2.1: +* Improved directory parsing +* Tweaked margin detection algorithm +* Improved error reporting + ####5.2: * Added new Panel View options * Implemented new margin detection algorithm diff --git a/gui/KCC.ui b/gui/KCC.ui index 3bdd9173..0b94d429 100644 --- a/gui/KCC.ui +++ b/gui/KCC.ui @@ -228,12 +228,12 @@ - + - <html><head/><body><p style='white-space:pre'>Create PNG files instead JPEG.<br/>Quality increase is not noticeable on most of devices.<br/>Output files <span style=" font-weight:600;">might</span> be smaller.<br/><span style=" font-weight:600;">MOBI conversion will be much slower.</span></p></body></html> + <html><head/><body><p style='white-space:pre'><span style=" font-weight:600; text-decoration: underline;">Unchecked - Automatic mode<br/></span>Output will be splitted automatically.</p><p style='white-space:pre'><span style=" font-weight:600; text-decoration: underline;">Checked - Volume mode<br/></span>Every subdirectory will be considered as separate volume.</p></body></html> - PNG output + Output split @@ -510,7 +510,7 @@ upscaleBox gammaBox borderBox - noDitheringBox + outputSplit colorBox editorButton wikiButton diff --git a/kcc.iss b/kcc.iss index 28a73176..9f0e8e63 100644 --- a/kcc.iss +++ b/kcc.iss @@ -1,5 +1,5 @@ #define MyAppName "Kindle Comic Converter" -#define MyAppVersion "5.2" +#define MyAppVersion "5.2.1" #define MyAppPublisher "Ciro Mattia Gonano, Paweł Jastrzębski" #define MyAppURL "http://kcc.iosphe.re/" #define MyAppExeName "KCC.exe" diff --git a/kcc/KCC_gui.py b/kcc/KCC_gui.py index dcc9e1bc..b60c2be3 100644 --- a/kcc/KCC_gui.py +++ b/kcc/KCC_gui.py @@ -21,17 +21,15 @@ import sys from urllib.parse import unquote from urllib.request import urlopen, urlretrieve, Request -from time import sleep, time -from datetime import datetime +from time import sleep from shutil import move from subprocess import STDOUT, PIPE from PyQt5 import QtGui, QtCore, QtWidgets, QtNetwork -from xml.dom.minidom import parse, Document +from xml.dom.minidom import parse from psutil import Popen, Process from copy import copy from distutils.version import StrictVersion from xml.sax.saxutils import escape -from platform import platform from raven import Client from .shared import md5Checksum, HTMLStripper, sanitizeTrace, saferRemove from . import __version__ @@ -270,8 +268,8 @@ def run(self): options.white_borders = True elif GUI.borderBox.checkState() == 2: options.black_borders = True - if GUI.noDitheringBox.isChecked(): - options.forcepng = True + if GUI.outputSplit.isChecked(): + options.batchsplit = 2 if GUI.colorBox.isChecked(): options.forcecolor = True if GUI.currentMode > 2: @@ -319,10 +317,15 @@ def run(self): GUI.progress.content = '' self.errors = True _, _, traceback = sys.exc_info() + if len(err.args) == 1: + MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s" + % (jobargv[-1], str(err), sanitizeTrace(traceback)), 'error') + else: + MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s" + % (jobargv[-1], str(err.args[0]), err.args[1]), 'error') + GUI.sentry.extra_context({'realTraceback': err.args[1]}) if ' is corrupted.' not in str(err): GUI.sentry.captureException() - MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s" - % (jobargv[-1], str(err), sanitizeTrace(traceback)), 'error') MW.addMessage.emit('Error during conversion! Please consult ' 'wiki ' 'for more details.', 'error', False) @@ -528,7 +531,7 @@ def selectFileMetaEditor(self): def clearJobs(self): GUI.jobList.clear() - # noinspection PyCallByClass,PyTypeChecker,PyArgumentList + # noinspection PyCallByClass,PyTypeChecker def openWiki(self): QtGui.QDesktopServices.openUrl(QtCore.QUrl('https://github.com/ciromattia/kcc/wiki')) @@ -646,6 +649,11 @@ def changeFormat(self, outputFormat=None): else: GUI.formatBox.setCurrentIndex(profile['DefaultFormat']) GUI.qualityBox.setEnabled(profile['PVOptions']) + if str(GUI.formatBox.currentText()) == 'MOBI/AZW3': + GUI.outputSplit.setEnabled(True) + else: + GUI.outputSplit.setEnabled(False) + GUI.outputSplit.setChecked(False) def stripTags(self, html): s = HTMLStripper() @@ -700,7 +708,6 @@ def convertStart(self): self.conversionAlive = False self.worker.sync() else: - # noinspection PyArgumentList if QtWidgets.QApplication.keyboardModifiers() == QtCore.Qt.ShiftModifier: dname = QtWidgets.QFileDialog.getExistingDirectory(MW, 'Select output directory', self.lastPath) if dname != '': @@ -762,7 +769,7 @@ def saveSettings(self, event): 'upscaleBox': GUI.upscaleBox.checkState(), 'borderBox': GUI.borderBox.checkState(), 'webtoonBox': GUI.webtoonBox.checkState(), - 'noDitheringBox': GUI.noDitheringBox.checkState(), + 'outputSplit': GUI.outputSplit.checkState(), 'colorBox': GUI.colorBox.checkState(), 'widthBox': GUI.widthBox.value(), 'heightBox': GUI.heightBox.value(), @@ -848,7 +855,6 @@ def detectKindleGen(self, startup=False): else: self.addMessage('Download it and place executable in /usr/local/bin directory.', 'error') - # noinspection PyArgumentList def __init__(self, KCCAplication, KCCWindow): global APP, MW, GUI APP = KCCAplication diff --git a/kcc/KCC_ui.py b/kcc/KCC_ui.py index 5e718ef2..6e2ed50a 100644 --- a/kcc/KCC_ui.py +++ b/kcc/KCC_ui.py @@ -97,9 +97,9 @@ def setupUi(self, mainWindow): self.borderBox.setTristate(True) self.borderBox.setObjectName("borderBox") self.gridLayout_2.addWidget(self.borderBox, 2, 0, 1, 1) - self.noDitheringBox = QtWidgets.QCheckBox(self.optionWidget) - self.noDitheringBox.setObjectName("noDitheringBox") - self.gridLayout_2.addWidget(self.noDitheringBox, 2, 1, 1, 1) + self.outputSplit = QtWidgets.QCheckBox(self.optionWidget) + self.outputSplit.setObjectName("outputSplit") + self.gridLayout_2.addWidget(self.outputSplit, 2, 1, 1, 1) self.colorBox = QtWidgets.QCheckBox(self.optionWidget) self.colorBox.setObjectName("colorBox") self.gridLayout_2.addWidget(self.colorBox, 2, 2, 1, 1) @@ -219,8 +219,8 @@ def setupUi(self, mainWindow): mainWindow.setTabOrder(self.webtoonBox, self.upscaleBox) mainWindow.setTabOrder(self.upscaleBox, self.gammaBox) mainWindow.setTabOrder(self.gammaBox, self.borderBox) - mainWindow.setTabOrder(self.borderBox, self.noDitheringBox) - mainWindow.setTabOrder(self.noDitheringBox, self.colorBox) + mainWindow.setTabOrder(self.borderBox, self.outputSplit) + mainWindow.setTabOrder(self.outputSplit, self.colorBox) mainWindow.setTabOrder(self.colorBox, self.editorButton) mainWindow.setTabOrder(self.editorButton, self.wikiButton) mainWindow.setTabOrder(self.wikiButton, self.jobList) @@ -251,8 +251,8 @@ def retranslateUi(self, mainWindow): self.gammaBox.setText(_translate("mainWindow", "Custom gamma")) self.borderBox.setToolTip(_translate("mainWindow", "

Unchecked - Autodetection
Color of margins fill will be detected automatically.

Indeterminate - White
Margins will be filled with white color.

Checked - Black
Margins will be filled with black color.

")) self.borderBox.setText(_translate("mainWindow", "W/B margins")) - self.noDitheringBox.setToolTip(_translate("mainWindow", "

Create PNG files instead JPEG.
Quality increase is not noticeable on most of devices.
Output files might be smaller.
MOBI conversion will be much slower.

")) - self.noDitheringBox.setText(_translate("mainWindow", "PNG output")) + self.outputSplit.setToolTip(_translate("mainWindow", "

Unchecked - Automatic mode
Output will be splitted automatically.

Checked - Volume mode
Every subdirectory will be considered as separate volume.

")) + self.outputSplit.setText(_translate("mainWindow", "Output split")) self.colorBox.setToolTip(_translate("mainWindow", "

Disable conversion to grayscale.

")) self.colorBox.setText(_translate("mainWindow", "Color mode")) self.gammaLabel.setText(_translate("mainWindow", "Gamma: Auto")) diff --git a/kcc/__init__.py b/kcc/__init__.py index d4f4bd0a..8fd5a243 100644 --- a/kcc/__init__.py +++ b/kcc/__init__.py @@ -1,4 +1,4 @@ -__version__ = '5.2' +__version__ = '5.2.1' __license__ = 'ISC' __copyright__ = '2012-2016, Ciro Mattia Gonano , Pawel Jastrzebski ' __docformat__ = 'restructuredtext en' diff --git a/kcc/comic2ebook.py b/kcc/comic2ebook.py index da9c906f..e16ea4f2 100755 --- a/kcc/comic2ebook.py +++ b/kcc/comic2ebook.py @@ -46,7 +46,7 @@ from scandir import walk except ImportError: walk = os.walk -from .shared import md5Checksum, getImageFileName, walkSort, walkLevel, saferReplace, saferRemove +from .shared import md5Checksum, getImageFileName, walkSort, walkLevel, saferReplace, saferRemove, sanitizeTrace from . import comic2panel from . import image from . import cbxarchive @@ -93,7 +93,6 @@ def buildHTML(path, imgfile, imgfilepath): additionalStyle = 'background-color:#000000;' else: additionalStyle = 'background-color:#FFFFFF;' - htmlpath = '' postfix = '' backref = 1 head = path @@ -483,7 +482,7 @@ def imgDirectoryProcessing(path): raise UserWarning("Conversion interrupted.") if len(workerOutput) > 0: rmtree(os.path.join(path, '..', '..'), True) - raise RuntimeError("One of workers crashed. Cause: " + workerOutput[0]) + raise RuntimeError("One of workers crashed. Cause: " + workerOutput[0][0], workerOutput[0][1]) for file in options.imgPurgeIndex: if os.path.isfile(file): saferRemove(file) @@ -493,7 +492,7 @@ def imgDirectoryProcessing(path): def imgFileProcessingTick(output): - if isinstance(output, str): + if isinstance(output, tuple): workerOutput.append(output) workerPool.terminate() else: @@ -527,7 +526,7 @@ def imgFileProcessing(work): output.append(img.saveToDir()) return output except Exception: - return str(sys.exc_info()[:2]) + return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2]) def getWorkFolder(afile): @@ -734,63 +733,25 @@ def sanitizePermissions(filetree): os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD | S_IEXEC) -# noinspection PyUnboundLocalVariable def splitDirectory(path): - # Detect directory stucture - for root, dirs, files in walkLevel(os.path.join(path, 'OEBPS', 'Images'), 0): - subdirectoryNumber = len(dirs) - filesNumber = len(files) - if subdirectoryNumber == 0: - # No subdirectories - mode = 0 + level = -1 + for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')): + for f in files: + if f.endswith('.jpg') or f.endswith('.jpeg') or f.endswith('.png') or f.endswith('.gif'): + newLevel = os.path.join(root, f).replace(os.path.join(path, 'OEBPS', 'Images'), '').count(os.sep) + if level != -1 and level != newLevel: + level = 0 + break + else: + level = newLevel + if level > 0: + splitter = splitProcess(os.path.join(path, 'OEBPS', 'Images'), level) + path = [path] + for tome in splitter: + path.append(tome) + return path else: - if filesNumber > 0: - print('WARNING: Automatic output splitting failed.') - if GUI: - GUI.addMessage.emit('Automatic output splitting failed. ' - 'More details.', 'warning', False) - GUI.addMessage.emit('', '', False) - return [path] - detectedSubSubdirectories = False - detectedFilesInSubdirectories = False - for root, dirs, files in walkLevel(os.path.join(path, 'OEBPS', 'Images'), 1): - if root != os.path.join(path, 'OEBPS', 'Images'): - if len(dirs) != 0: - detectedSubSubdirectories = True - elif len(dirs) == 0 and detectedSubSubdirectories: - print('WARNING: Automatic output splitting failed.') - if GUI: - GUI.addMessage.emit('Automatic output splitting failed. ' - 'More details.', 'warning', False) - GUI.addMessage.emit('', '', False) - return [path] - if len(files) != 0: - detectedFilesInSubdirectories = True - if detectedSubSubdirectories: - # Two levels of subdirectories - mode = 2 - else: - # One level of subdirectories - mode = 1 - if detectedFilesInSubdirectories and detectedSubSubdirectories: - print('WARNING: Automatic output splitting failed.') - if GUI: - GUI.addMessage.emit('Automatic output splitting failed. ' - 'More details.', 'warning', False) - GUI.addMessage.emit('', '', False) - return [path] - # Split directories - splitter = splitProcess(os.path.join(path, 'OEBPS', 'Images'), mode) - path = [path] - for tome in splitter: - path.append(tome) - return path + raise UserWarning('Unsupported directory structure.') def splitProcess(path, mode): @@ -801,10 +762,15 @@ def splitProcess(path, mode): targetSize = 104857600 else: targetSize = 419430400 - if mode == 0: + if options.batchsplit == 2 and mode == 2: + mode = 3 + if mode < 3: for root, dirs, files in walkLevel(path, 0): - for name in files: - size = os.path.getsize(os.path.join(root, name)) + for name in files if mode == 1 else dirs: + if mode == 1: + size = os.path.getsize(os.path.join(root, name)) + else: + size = getDirectorySize(os.path.join(root, name)) if currentSize + size > targetSize: currentTarget, pathRoot = createNewTome() output.append(pathRoot) @@ -813,48 +779,16 @@ def splitProcess(path, mode): currentSize += size if path != currentTarget: move(os.path.join(root, name), os.path.join(currentTarget, name)) - elif mode == 1: + else: + firstTome = True for root, dirs, files in walkLevel(path, 0): for name in dirs: - size = getDirectorySize(os.path.join(root, name)) - if currentSize + size > targetSize: + if not firstTome: currentTarget, pathRoot = createNewTome() output.append(pathRoot) - currentSize = size - else: - currentSize += size - if path != currentTarget: move(os.path.join(root, name), os.path.join(currentTarget, name)) - elif mode == 2: - firstTome = True - for root, dirs, files in walkLevel(path, 0): - for name in dirs: - size = getDirectorySize(os.path.join(root, name)) - currentSize = 0 - if size > targetSize: - if not firstTome: - currentTarget, pathRoot = createNewTome() - output.append(pathRoot) - else: - firstTome = False - for rootInside, dirsInside, filesInside in walkLevel(os.path.join(root, name), 0): - for nameInside in dirsInside: - size = getDirectorySize(os.path.join(rootInside, nameInside)) - if currentSize + size > targetSize: - currentTarget, pathRoot = createNewTome() - output.append(pathRoot) - currentSize = size - else: - currentSize += size - if path != currentTarget: - move(os.path.join(rootInside, nameInside), os.path.join(currentTarget, nameInside)) else: - if not firstTome: - currentTarget, pathRoot = createNewTome() - output.append(pathRoot) - move(os.path.join(root, name), os.path.join(currentTarget, name)) - else: - firstTome = False + firstTome = False return output @@ -947,8 +881,9 @@ def makeParser(): help="Comic title [Default=filename or directory name]") outputOptions.add_option("-f", "--format", action="store", dest="format", default="Auto", help="Output format (Available options: Auto, MOBI, EPUB, CBZ) [Default=Auto]") - outputOptions.add_option("-b", "--batchsplit", action="store_true", dest="batchsplit", default=False, - help="Split output into multiple files"), + outputOptions.add_option("-b", "--batchsplit", type="int", dest="batchsplit", default="0", + help="Split output into multiple files. 0: Don't split 1: Automatic mode " + "2: Consider every subdirectory as separate volume [Default=0]") processingOptions.add_option("-u", "--upscale", action="store_true", dest="upscale", default=False, help="Resize images smaller than device's resolution") @@ -1006,8 +941,8 @@ def checkOptions(): if options.black_borders: options.bordersColor = 'black' # Splitting MOBI is not optional - if options.format == 'MOBI': - options.batchsplit = True + if options.format == 'MOBI' and options.batchsplit != 2: + options.batchsplit = 1 # Older Kindle don't need higher resolution files due lack of Panel View. if options.profile == 'K1' or options.profile == 'K2' or options.profile == 'K3' or options.profile == 'KDX': options.panelview = False @@ -1092,8 +1027,8 @@ def makeBook(source, qtGUI=None): getComicInfo(os.path.join(path, "OEBPS", "Images"), source) detectCorruption(os.path.join(path, "OEBPS", "Images"), source) if options.webtoon: - if image.ProfileData.Profiles[options.profile][1][1] > 1000: - y = 1000 + if image.ProfileData.Profiles[options.profile][1][1] > 1024: + y = 1024 else: y = image.ProfileData.Profiles[options.profile][1][1] comic2panel.main(['-y ' + str(y), '-i', '-m', path], qtGUI) @@ -1106,7 +1041,7 @@ def makeBook(source, qtGUI=None): chapterNames = sanitizeTree(os.path.join(path, 'OEBPS', 'Images')) if 'Ko' in options.profile and options.format == 'CBZ': sanitizeTreeKobo(os.path.join(path, 'OEBPS', 'Images')) - if options.batchsplit: + if options.batchsplit > 0: tomes = splitDirectory(path) else: tomes = [path] diff --git a/kcc/comic2panel.py b/kcc/comic2panel.py index def82509..5e2b4f98 100644 --- a/kcc/comic2panel.py +++ b/kcc/comic2panel.py @@ -24,7 +24,7 @@ from optparse import OptionParser, OptionGroup from multiprocessing import Pool from PIL import Image, ImageStat, ImageOps -from .shared import getImageFileName, walkLevel, walkSort, saferRemove +from .shared import getImageFileName, walkLevel, walkSort, saferRemove, sanitizeTrace try: from PyQt5 import QtCore except ImportError: @@ -81,7 +81,7 @@ def mergeDirectory(work): savePath = os.path.split(imagesValid[0]) result.save(os.path.join(savePath[0], os.path.splitext(savePath[1])[0] + '.png'), 'PNG') except Exception: - return str(sys.exc_info()[1]) + return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2]) def sanitizePanelSize(panel, opt): @@ -205,7 +205,7 @@ def splitImage(work): pageNumber += 1 saferRemove(filePath) except Exception: - return str(sys.exc_info()[1]) + return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2]) def main(argv=None, qtGUI=None): @@ -267,7 +267,7 @@ def main(argv=None, qtGUI=None): raise UserWarning("Conversion interrupted.") if len(mergeWorkerOutput) > 0: rmtree(options.targetDir, True) - raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0]) + raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0], mergeWorkerOutput[0][1]) print("Splitting images...") for root, dirs, files in walk(options.targetDir, False): for name in files: @@ -290,7 +290,7 @@ def main(argv=None, qtGUI=None): raise UserWarning("Conversion interrupted.") if len(splitWorkerOutput) > 0: rmtree(options.targetDir, True) - raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0]) + raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0], splitWorkerOutput[0][1]) if options.inPlace: rmtree(options.sourceDir) move(options.targetDir, options.sourceDir) diff --git a/kcc/image.py b/kcc/image.py index 009b2bbf..682ae454 100755 --- a/kcc/image.py +++ b/kcc/image.py @@ -318,7 +318,7 @@ def cropPageNumber(self, power): tmpImg = tmpImg.point(lambda x: x and 255) tmpImg = tmpImg.filter(ImageFilter.MinFilter(size=3)) tmpImg = tmpImg.filter(ImageFilter.GaussianBlur(radius=5)) - tmpImg = tmpImg.point(lambda x: (x >= 48 * power) and x) + tmpImg = tmpImg.point(lambda x: (x >= 16 * power) and x) self.image = self.image.crop(tmpImg.getbbox()) if tmpImg.getbbox() else self.image def cropMargin(self, power): diff --git a/kcc/pdfjpgextract.py b/kcc/pdfjpgextract.py index f4d5813c..06403adb 100644 --- a/kcc/pdfjpgextract.py +++ b/kcc/pdfjpgextract.py @@ -61,7 +61,6 @@ def extract(self): iend += endfix jpg = pdf[istart:iend] jpgfile = open(self.path + "/jpg%d.jpg" % njpg, "wb") - # noinspection PyTypeChecker jpgfile.write(jpg) jpgfile.close() njpg += 1 diff --git a/kcc/shared.py b/kcc/shared.py index 88d1782d..c228d30f 100644 --- a/kcc/shared.py +++ b/kcc/shared.py @@ -144,7 +144,10 @@ def removeFromZIP(zipfname, *filenames): def sanitizeTrace(traceback): return ''.join(format_tb(traceback))\ - .replace('C:\\Users\\Pawel\\Documents\\Projekty\\KCC\\', '') \ + .replace('C:/Users/Pawel/Documents/Projekty/KCC/', '')\ + .replace('C:/Python35/', '')\ + .replace('c:/python35/', '')\ + .replace('C:\\Users\\Pawel\\Documents\\Projekty\\KCC\\', '')\ .replace('C:\\Python35\\', '')\ .replace('c:\\python35\\', '') diff --git a/other/osx/Info.plist b/other/osx/Info.plist index d883b4d2..0eb9c942 100644 --- a/other/osx/Info.plist +++ b/other/osx/Info.plist @@ -9,7 +9,7 @@ CFBundleExecutable MacOS/Kindle Comic Converter CFBundleGetInfoString - KindleComicConverter 5.2, written 2012-2016 by Ciro Mattia Gonano and Pawel Jastrzebski + KindleComicConverter 5.2.1, written 2012-2016 by Ciro Mattia Gonano and Pawel Jastrzebski CFBundleIconFile comic2ebook.icns CFBundleIdentifier @@ -21,11 +21,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 5.2.0 + 5.2.1 CFBundleSignature ???? CFBundleVersion - 5.2.0 + 5.2.1 LSEnvironment PATH