diff options
| -rw-r--r-- | README.md | 10 | ||||
| -rw-r--r-- | gui/KCC-OSX.ui | 6 | ||||
| -rw-r--r-- | icons/WizardOSX.png | bin | 0 -> 335950 bytes | |||
| -rw-r--r-- | kcc.iss | 2 | ||||
| -rw-r--r-- | kcc/KCC_gui.py | 65 | ||||
| -rw-r--r-- | kcc/KCC_ui_osx.py | 2 | ||||
| -rw-r--r-- | kcc/__init__.py | 2 | ||||
| -rw-r--r-- | kcc/cbxarchive.py | 4 | ||||
| -rwxr-xr-x | kcc/comic2ebook.py | 67 | ||||
| -rw-r--r-- | kcc/comic2panel.py | 20 | ||||
| -rw-r--r-- | kcc/dualmetafix.py | 49 | ||||
| -rwxr-xr-x | kcc/image.py | 56 | ||||
| -rw-r--r-- | kcc/rarfile.py | 5 | ||||
| -rw-r--r-- | kcc/shared.py | 24 | ||||
| -rw-r--r-- | setup.json | 10 | ||||
| -rwxr-xr-x | setup.py | 19 |
16 files changed, 190 insertions, 151 deletions
diff --git a/README.md b/README.md index 5b88121..0d22719 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,10 @@ You can find the latest released binary at the following links: ## DEPENDENCIES Following software is required to run Linux version of **KCC** and/or bare sources: - Python 3.3+ -- [PyQt](http://www.riverbankcomputing.co.uk/software/pyqt/download5) 5.2.0+ +- [PyQt](http://www.riverbankcomputing.co.uk/software/pyqt/download5) 5.4.0+ - [Pillow](http://pypi.python.org/pypi/Pillow/) 2.8.2+ - [psutil](https://pypi.python.org/pypi/psutil) 3.0.0+ -- [python-slugify](http://pypi.python.org/pypi/python-slugify) 1.1.2+ +- [python-slugify](http://pypi.python.org/pypi/python-slugify) 1.1.3+ - [scandir](https://pypi.python.org/pypi/scandir) 1.1.0+ On Debian based distributions these two commands should install all needed dependencies: @@ -156,6 +156,12 @@ The app relies and includes the following scripts: * [Kobo Aura H2O](http://kcc.iosphe.re/Samples/Ubunchu-KoAH2O.kepub.epub) ## CHANGELOG +####4.6.4: +* Fixed multiple Windows specific problems +* Improved error handling +* Improved color detection algorithm +* New, slimmer OS X release + ####4.6.3: * Implemented remote bug reporting * Minor bug fixes and GUI tweaks diff --git a/gui/KCC-OSX.ui b/gui/KCC-OSX.ui index 19768ee..7000969 100644 --- a/gui/KCC-OSX.ui +++ b/gui/KCC-OSX.ui @@ -462,6 +462,12 @@ <property name="selectionMode"> <enum>QAbstractItemView::NoSelection</enum> </property> + <property name="verticalScrollMode"> + <enum>QAbstractItemView::ScrollPerPixel</enum> + </property> + <property name="horizontalScrollMode"> + <enum>QAbstractItemView::ScrollPerPixel</enum> + </property> </widget> <widget class="QPushButton" name="BasicModeButton"> <property name="geometry"> diff --git a/icons/WizardOSX.png b/icons/WizardOSX.png new file mode 100644 index 0000000..8da4e66 --- /dev/null +++ b/icons/WizardOSX.png Binary files differdiff --git a/kcc.iss b/kcc.iss index d4e88fe..eb720e8 100644 --- a/kcc.iss +++ b/kcc.iss @@ -1,5 +1,5 @@ #define MyAppName "Kindle Comic Converter" -#define MyAppVersion "4.6.3" +#define MyAppVersion "4.6.4" #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 e5948e9..45e5ba7 100644 --- a/kcc/KCC_gui.py +++ b/kcc/KCC_gui.py @@ -22,7 +22,6 @@ import sys from urllib.parse import unquote from urllib.request import urlopen, urlretrieve, Request from socket import gethostbyname_ex, gethostname -from traceback import format_tb from time import sleep, time from datetime import datetime from shutil import move @@ -36,7 +35,7 @@ from copy import copy from distutils.version import StrictVersion from xml.sax.saxutils import escape from platform import platform -from .shared import md5Checksum, HTMLStripper +from .shared import md5Checksum, HTMLStripper, sanitizeTrace from . import __version__ from . import comic2ebook from . import KCC_rc_web @@ -67,7 +66,6 @@ class QApplicationMessaging(QtWidgets.QApplication): socket.connectToServer(self._key, QtCore.QIODevice.WriteOnly) if not socket.waitForConnected(self._timeout): self._server = QtNetwork.QLocalServer(self) - # noinspection PyUnresolvedReferences self._server.newConnection.connect(self.handleMessage) self._server.listen(self._key) else: @@ -140,7 +138,7 @@ class Icons: class WebServerHandler(BaseHTTPRequestHandler): - # noinspection PyAttributeOutsideInit, PyArgumentList + # noinspection PyAttributeOutsideInit def do_GET(self): if self.path == '/': self.path = '/index.html' @@ -277,8 +275,8 @@ class VersionThread(QtCore.QThread): try: MW.modeConvert.emit(-1) MW.progressBarTick.emit('Downloading update') - path = urlretrieve('https://kcc.iosphe.re/Windows/KindleComicConverter_win_' - + self.newVersion + '.exe', reporthook=self.getNewVersionTick) + path = urlretrieve('https://kcc.iosphe.re/Windows/KindleComicConverter_win_' + + self.newVersion + '.exe', reporthook=self.getNewVersionTick) if self.md5 != md5Checksum(path[0]): raise Exception move(path[0], path[0] + '.exe') @@ -325,7 +323,6 @@ class ProgressThread(QtCore.QThread): class WorkerThread(QtCore.QThread): - # noinspection PyArgumentList def __init__(self): QtCore.QThread.__init__(self) self.conversionAlive = False @@ -350,12 +347,6 @@ class WorkerThread(QtCore.QThread): MW.addTrayMessage.emit('Conversion interrupted.', 'Critical') MW.modeConvert.emit(1) - def sanitizeTrace(self, traceback): - return ''.join(format_tb(traceback))\ - .replace('C:\\Users\\pawel\\Documents\\Projekty\\KCC\\', '')\ - .replace('C:\\Python34\\', '')\ - .replace('C:\\Python34_64\\', '') - def run(self): MW.modeConvert.emit(0) @@ -439,16 +430,20 @@ class WorkerThread(QtCore.QThread): GUI.progress.content = '' self.errors = True MW.addMessage.emit(str(warn), 'warning', False) - MW.addMessage.emit('Failed to create output file!', 'error', False) - MW.addTrayMessage.emit('Failed to create output file!', 'Critical') + 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) + MW.addTrayMessage.emit('Error during conversion!', 'Critical') except Exception as err: GUI.progress.content = '' self.errors = True _, _, traceback = sys.exc_info() MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s" - % (jobargv[-1], str(err), self.sanitizeTrace(traceback)), 'error') - MW.addMessage.emit('Failed to create EPUB!', 'error', False) - MW.addTrayMessage.emit('Failed to create EPUB!', 'Critical') + % (jobargv[-1], str(err), sanitizeTrace(traceback)), 'error') + 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) + MW.addTrayMessage.emit('Error during conversion!', 'Critical') if not self.conversionAlive: for item in outputPath: if os.path.exists(item): @@ -463,7 +458,7 @@ class WorkerThread(QtCore.QThread): MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True) if str(GUI.FormatBox.currentText()) == 'MOBI': MW.progressBarTick.emit('Creating MOBI files') - MW.progressBarTick.emit(str(len(outputPath)*2+1)) + MW.progressBarTick.emit(str(len(outputPath) * 2 + 1)) MW.progressBarTick.emit('tick') MW.addMessage.emit('Creating MOBI files', 'info', False) GUI.progress.content = 'Creating MOBI files' @@ -502,7 +497,7 @@ class WorkerThread(QtCore.QThread): GUI.progress.content = '' mobiPath = item.replace('.epub', '.mobi') os.remove(mobiPath + '_toclean') - if GUI.targetDirectory and GUI.targetDirectory != os.path.split(mobiPath)[0]: + if GUI.targetDirectory and GUI.targetDirectory != os.path.dirname(mobiPath): try: move(mobiPath, GUI.targetDirectory) mobiPath = os.path.join(GUI.targetDirectory, os.path.basename(mobiPath)) @@ -522,7 +517,7 @@ class WorkerThread(QtCore.QThread): MW.addTrayMessage.emit('Failed to process MOBI file!', 'Critical') else: GUI.progress.content = '' - epubSize = (os.path.getsize(self.kindlegenErrorCode[2]))//1024//1024 + epubSize = (os.path.getsize(self.kindlegenErrorCode[2])) // 1024 // 1024 for item in outputPath: if os.path.exists(item): os.remove(item) @@ -538,7 +533,7 @@ class WorkerThread(QtCore.QThread): False) else: for item in outputPath: - if GUI.targetDirectory and GUI.targetDirectory != os.path.split(item)[0]: + if GUI.targetDirectory and GUI.targetDirectory != os.path.dirname(item): try: move(item, GUI.targetDirectory) item = os.path.join(GUI.targetDirectory, os.path.basename(item)) @@ -549,8 +544,9 @@ class WorkerThread(QtCore.QThread): GUI.progress.stop() MW.hideProgressBar.emit() GUI.needClean = True - MW.addMessage.emit('<b>All jobs completed.</b>', 'info', False) - MW.addTrayMessage.emit('All jobs completed.', 'Information') + if not self.errors: + MW.addMessage.emit('<b>All jobs completed.</b>', 'info', False) + MW.addTrayMessage.emit('All jobs completed.', 'Information') MW.modeConvert.emit(1) @@ -559,7 +555,6 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): super().__init__() if self.isSystemTrayAvailable(): QtWidgets.QSystemTrayIcon.__init__(self, GUI.icons.programIcon, MW) - # noinspection PyUnresolvedReferences self.activated.connect(self.catchClicks) def catchClicks(self): @@ -635,8 +630,10 @@ class KCCGUI(KCC_ui.Ui_KCC): self.lastPath = os.path.abspath(os.path.join(fname, os.pardir)) try: self.editor.loadData(fname) - except: - self.showDialog('Failed to parse metadata!', 'error') + except Exception as err: + _, _, traceback = sys.exc_info() + self.showDialog("Failed to parse metadata!\n\n%s\n\nTraceback:\n%s" + % (str(err), sanitizeTrace(traceback)), 'error') else: self.editor.ui.exec_() @@ -845,7 +842,7 @@ class KCCGUI(KCC_ui.Ui_KCC): def changeGamma(self, value): value = float(value) - value = '%.2f' % (value/100) + value = '%.2f' % (value / 100) if float(value) <= 0.09: GUI.GammaLabel.setText('Gamma: Auto') else: @@ -913,7 +910,7 @@ class KCCGUI(KCC_ui.Ui_KCC): else: item = QtWidgets.QListWidgetItem(' ' + self.stripTags(message)) if replace: - GUI.JobList.takeItem(GUI.JobList.count()-1) + GUI.JobList.takeItem(GUI.JobList.count() - 1) # Due to lack of HTML support in QListWidgetItem we overlay text field with QLabel # We still fill original text field with transparent content to trigger creation of horizontal scrollbar item.setForeground(QtGui.QColor('transparent')) @@ -1053,7 +1050,7 @@ class KCCGUI(KCC_ui.Ui_KCC): 'ColorBox': GUI.ColorBox.checkState(), 'customWidth': GUI.customWidth.text(), 'customHeight': GUI.customHeight.text(), - 'GammaSlider': float(self.GammaValue)*100}) + 'GammaSlider': float(self.GammaValue) * 100}) self.settings.sync() self.tray.hide() @@ -1300,7 +1297,7 @@ class KCCGUI(KCC_ui.Ui_KCC): if profile == "Other": GUI.DeviceBox.addItem(self.icons.deviceOther, profile) elif profile == "Separator": - GUI.DeviceBox.insertSeparator(GUI.DeviceBox.count()+1) + GUI.DeviceBox.insertSeparator(GUI.DeviceBox.count() + 1) elif 'Ko' in profile: GUI.DeviceBox.addItem(self.icons.deviceKobo, profile) else: @@ -1381,8 +1378,10 @@ class KCCGUI_MetaEditor(KCC_MetaEditor_ui.Ui_MetaEditorDialog): self.parser.data[field.objectName()[:-4] + 's'] = tmpData try: self.parser.saveXML() - except: - GUI.showDialog('Failed to save metadata!', 'error') + 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): diff --git a/kcc/KCC_ui_osx.py b/kcc/KCC_ui_osx.py index 6184b04..55f1a19 100644 --- a/kcc/KCC_ui_osx.py +++ b/kcc/KCC_ui_osx.py @@ -188,6 +188,8 @@ class Ui_KCC(object): self.JobList.setStyleSheet("QListWidget#JobList {background:#ffffff;background-image:url(:/Other/icons/list_background.png);background-position:center center;background-repeat:no-repeat;}QScrollBar:vertical{border:1px solid #999;background:#FFF;width:5px;margin:0}QScrollBar::handle:vertical{background:DarkGray;min-height:0}QScrollBar::add-line:vertical{height:0;background:DarkGray;subcontrol-position:bottom;subcontrol-origin:margin}QScrollBar::sub-line:vertical{height:0;background:DarkGray;subcontrol-position:top;subcontrol-origin:margin}QScrollBar:horizontal{border:1px solid #999;background:#FFF;height:5px;margin:0}QScrollBar::handle:horizontal{background:DarkGray;min-width:0}QScrollBar::add-line:horizontal{width:0;background:DarkGray;subcontrol-position:bottom;subcontrol-origin:margin}QScrollBar::sub-line:horizontal{width:0;background:DarkGray;subcontrol-position:top;subcontrol-origin:margin}") self.JobList.setProperty("showDropIndicator", False) self.JobList.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + self.JobList.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) + self.JobList.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) self.JobList.setObjectName("JobList") self.BasicModeButton = QtWidgets.QPushButton(self.Form) self.BasicModeButton.setGeometry(QtCore.QRect(5, 10, 156, 41)) diff --git a/kcc/__init__.py b/kcc/__init__.py index 559ed65..6d9142d 100644 --- a/kcc/__init__.py +++ b/kcc/__init__.py @@ -1,4 +1,4 @@ -__version__ = '4.6.3' +__version__ = '4.6.4' __license__ = 'ISC' __copyright__ = '2012-2015, Ciro Mattia Gonano <[email protected]>, Pawel Jastrzebski <[email protected]>' __docformat__ = 'restructuredtext en' diff --git a/kcc/cbxarchive.py b/kcc/cbxarchive.py index 823a4c8..9332eef 100644 --- a/kcc/cbxarchive.py +++ b/kcc/cbxarchive.py @@ -70,8 +70,8 @@ class CBxArchive: if sys.platform.startswith('darwin'): copy(self.origFileName, os.path.join(os.path.dirname(self.origFileName), 'TMP_KCC_TMP')) self.origFileName = os.path.join(os.path.dirname(self.origFileName), 'TMP_KCC_TMP') - output = Popen('7za x "' + self.origFileName + '" -xr!__MACOSX -xr!.DS_Store -xr!thumbs.db -xr!Thumbs.db -o"' - + targetdir + '"', stdout=PIPE, stderr=STDOUT, shell=True) + output = Popen('7za x "' + self.origFileName + '" -xr!__MACOSX -xr!.DS_Store -xr!thumbs.db -xr!Thumbs.db -o"' + + targetdir + '"', stdout=PIPE, stderr=STDOUT, shell=True) extracted = False for line in output.stdout: if b"Everything is Ok" in line: diff --git a/kcc/comic2ebook.py b/kcc/comic2ebook.py index ce4cfa6..0fc3db7 100755 --- a/kcc/comic2ebook.py +++ b/kcc/comic2ebook.py @@ -28,7 +28,7 @@ from urllib.request import Request, urlopen from re import sub from stat import S_IWRITE, S_IREAD, S_IEXEC from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED -from tempfile import mkdtemp +from tempfile import mkdtemp, gettempdir from shutil import move, copytree, rmtree from optparse import OptionParser, OptionGroup from multiprocessing import Pool @@ -43,7 +43,7 @@ try: from PyQt5 import QtCore except ImportError: QtCore = None -from .shared import md5Checksum, getImageFileName, walkSort, walkLevel, saferReplace +from .shared import md5Checksum, getImageFileName, walkSort, walkLevel, saferReplace from . import comic2panel from . import image from . import cbxarchive @@ -66,6 +66,7 @@ def main(argv=None): print('No matching files found.') return for source in sources: + source = source.rstrip('\\').rstrip('/') options = copy(optionstemplate) checkOptions() if len(sources) > 1: @@ -242,9 +243,9 @@ def buildNCX(dstdir, title, chapters, chapterNames): navID = filename[0].replace('/', '_').replace('\\', '_') elif os.path.basename(folder) != "Text": title = chapterNames[os.path.basename(folder)] - f.write("<navPoint id=\"" + navID + "\"><navLabel><text>" - + title + "</text></navLabel><content src=\"" + filename[0].replace("\\", "/") - + ".html\"/></navPoint>\n") + f.write("<navPoint id=\"" + navID + "\"><navLabel><text>" + + title + "</text></navLabel><content src=\"" + filename[0].replace("\\", "/") + + ".html\"/></navPoint>\n") f.write("</navMap>\n</ncx>") f.close() @@ -341,15 +342,15 @@ def buildOPF(dstdir, title, filelist, cover=None): filename = getImageFileName(path[1]) uniqueid = os.path.join(folder, filename[0]).replace('/', '_').replace('\\', '_') reflist.append(uniqueid) - f.write("<item id=\"page_" + str(uniqueid) + "\" href=\"" - + folder.replace('Images', 'Text') + "/" + filename[0] - + ".html\" media-type=\"application/xhtml+xml\"/>\n") + f.write("<item id=\"page_" + str(uniqueid) + "\" href=\"" + + folder.replace('Images', 'Text') + "/" + filename[0] + + ".html\" media-type=\"application/xhtml+xml\"/>\n") if '.png' == filename[1]: mt = 'image/png' else: mt = 'image/jpeg' - f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" - + mt + "\"/>\n") + f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" + + mt + "\"/>\n") f.write("<item id=\"css\" href=\"Text/style.css\" media-type=\"text/css\"/>\n") if options.righttoleft: f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n") @@ -647,44 +648,38 @@ def imgFileProcessing(work): def getWorkFolder(afile): - if len(afile) > 240: - raise UserWarning("Path is too long.") if os.path.isdir(afile): workdir = mkdtemp('', 'KCC-') try: os.rmdir(workdir) fullPath = os.path.join(workdir, 'OEBPS', 'Images') - if len(fullPath) > 240: - raise UserWarning("Path is too long.") copytree(afile, fullPath) sanitizePermissions(fullPath) return workdir - except OSError: + except: rmtree(workdir, True) - raise + raise UserWarning("Failed to prepare a workspace.") elif afile.lower().endswith('.pdf'): pdf = pdfjpgextract.PdfJpgExtract(afile) path, njpg = pdf.extract() if njpg == 0: rmtree(path, True) - raise UserWarning("Failed to extract images.") + raise UserWarning("Failed to extract images from PDF file.") else: workdir = mkdtemp('', 'KCC-') cbx = cbxarchive.CBxArchive(afile) if cbx.isCbxFile(): try: path = cbx.extract(workdir) - except OSError: + except: rmtree(workdir, True) - raise UserWarning("Failed to extract file.") + raise UserWarning("Failed to extract archive.") else: rmtree(workdir, True) - raise TypeError("Failed to detect archive format.") - if len(os.path.join(path, 'OEBPS', 'Images')) > 240: - raise UserWarning("Path is too long.") - move(path, path + "_temp") - move(path + "_temp", os.path.join(path, 'OEBPS', 'Images')) - return path + raise UserWarning("Failed to detect archive format.") + newpath = mkdtemp('', 'KCC-') + move(path, os.path.join(newpath, 'OEBPS', 'Images')) + return newpath def getOutputFilename(srcpath, wantedname, ext, tomeNumber): @@ -1039,7 +1034,7 @@ def createNewTome(): def slugify(value): value = slugifyExt(value) - value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value)) + value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value, count=2)) return value @@ -1181,7 +1176,7 @@ def checkOptions(): if options.customheight != 0: Y = options.customheight newProfile = ("Custom", (int(X), int(Y)), image.ProfileData.Palette16, - image.ProfileData.Profiles[options.profile][3], (int(int(X)*1.5), int(int(Y)*1.5))) + image.ProfileData.Profiles[options.profile][3], (int(int(X) * 1.5), int(int(Y) * 1.5))) image.ProfileData.Profiles["Custom"] = newProfile options.profile = "Custom" options.profileData = image.ProfileData.Profiles[options.profile] @@ -1208,6 +1203,21 @@ def checkTools(source): exit(1) +def checkPre(source): + # Make sure that all temporary files are gone + for root, dirs, _ in walkLevel(gettempdir(), 0): + for tempdir in dirs: + if tempdir.startswith('KCC-'): + rmtree(os.path.join(root, tempdir), True) + # Make sure that target directory is writable + if os.path.isdir(source): + writable = os.access(os.path.abspath(os.path.join(source, '..')), os.W_OK) + else: + writable = os.access(os.path.dirname(source), os.W_OK) + if not writable: + raise UserWarning("Target directory is not writable.") + + def makeBook(source, qtGUI=None): """Generates MOBI/EPUB/CBZ comic ebook from a bunch of images.""" global GUI @@ -1216,6 +1226,7 @@ def makeBook(source, qtGUI=None): GUI.progressBarTick.emit('1') else: checkTools(source) + checkPre(source) path = getWorkFolder(source) print("\nChecking images...") getComicInfo(os.path.join(path, "OEBPS", "Images"), source) @@ -1348,7 +1359,7 @@ def makeMOBI(work, qtGUI=None): global GUI, makeMOBIWorkerPool, makeMOBIWorkerOutput GUI = qtGUI makeMOBIWorkerOutput = [] - availableMemory = virtual_memory().total/1000000000 + availableMemory = virtual_memory().total / 1000000000 if availableMemory <= 2: threadNumber = 1 elif 2 < availableMemory <= 4: diff --git a/kcc/comic2panel.py b/kcc/comic2panel.py index c68103e..2bfe3d9 100644 --- a/kcc/comic2panel.py +++ b/kcc/comic2panel.py @@ -85,19 +85,19 @@ def sanitizePanelSize(panel, opt): newPanels = [] if panel[2] > 6 * opt.height: diff = int(panel[2] / 8) - newPanels.append([panel[0], panel[1] - diff*7, diff]) - newPanels.append([panel[1] - diff*7, panel[1] - diff*6, diff]) - newPanels.append([panel[1] - diff*6, panel[1] - diff*5, diff]) - newPanels.append([panel[1] - diff*5, panel[1] - diff*4, diff]) - newPanels.append([panel[1] - diff*4, panel[1] - diff*3, diff]) - newPanels.append([panel[1] - diff*3, panel[1] - diff*2, diff]) - newPanels.append([panel[1] - diff*2, panel[1] - diff, diff]) + newPanels.append([panel[0], panel[1] - diff * 7, diff]) + newPanels.append([panel[1] - diff * 7, panel[1] - diff * 6, diff]) + newPanels.append([panel[1] - diff * 6, panel[1] - diff * 5, diff]) + newPanels.append([panel[1] - diff * 5, panel[1] - diff * 4, diff]) + newPanels.append([panel[1] - diff * 4, panel[1] - diff * 3, diff]) + newPanels.append([panel[1] - diff * 3, panel[1] - diff * 2, diff]) + newPanels.append([panel[1] - diff * 2, panel[1] - diff, diff]) newPanels.append([panel[1] - diff, panel[1], diff]) elif panel[2] > 3 * opt.height: diff = int(panel[2] / 4) - newPanels.append([panel[0], panel[1] - diff*3, diff]) - newPanels.append([panel[1] - diff*3, panel[1] - diff*2, diff]) - newPanels.append([panel[1] - diff*2, panel[1] - diff, diff]) + newPanels.append([panel[0], panel[1] - diff * 3, diff]) + newPanels.append([panel[1] - diff * 3, panel[1] - diff * 2, diff]) + newPanels.append([panel[1] - diff * 2, panel[1] - diff, diff]) newPanels.append([panel[1] - diff, panel[1], diff]) elif panel[2] > 1.5 * opt.height: newPanels.append([panel[0], panel[1] - int(panel[2] / 2), int(panel[2] / 2)]) diff --git a/kcc/dualmetafix.py b/kcc/dualmetafix.py index 0eec68b..0d0ce04 100644 --- a/kcc/dualmetafix.py +++ b/kcc/dualmetafix.py @@ -36,15 +36,15 @@ title_offset = 84 def getint(data, ofs, sz='L'): - i, = struct.unpack_from('>'+sz, data, ofs) + i, = struct.unpack_from('>' + sz, data, ofs) return i def writeint(data, ofs, n, slen='L'): if slen == 'L': - return data[:ofs]+struct.pack('>L', n)+data[ofs+4:] + return data[:ofs] + struct.pack('>L', n) + data[ofs + 4:] else: - return data[:ofs]+struct.pack('>H', n)+data[ofs+2:] + return data[:ofs] + struct.pack('>H', n) + data[ofs + 2:] def getsecaddr(datain, secno): @@ -52,11 +52,11 @@ def getsecaddr(datain, secno): if (secno < 0) | (secno >= nsec): emsg = 'requested section number %d out of range (nsec=%d)' % (secno, nsec) raise DualMetaFixException(emsg) - secstart = getint(datain, first_pdb_record+secno*8) - if secno == nsec-1: + secstart = getint(datain, first_pdb_record + secno * 8) + if secno == nsec - 1: secend = len(datain) else: - secend = getint(datain, first_pdb_record+(secno+1)*8) + secend = getint(datain, first_pdb_record + (secno + 1) * 8) return secstart, secend @@ -71,28 +71,28 @@ def replacesection(datain, secno, secdata): seclen = secend - secstart if len(secdata) != seclen: raise DualMetaFixException('section length change in replacesection') - datain[secstart:secstart+seclen] = secdata + datain[secstart:secstart + seclen] = secdata def get_exth_params(rec0): ebase = mobi_header_base + getint(rec0, mobi_header_length) - if rec0[ebase:ebase+4] != b'EXTH': + if rec0[ebase:ebase + 4] != b'EXTH': raise DualMetaFixException('EXTH tag not found where expected') - elen = getint(rec0, ebase+4) - enum = getint(rec0, ebase+8) + elen = getint(rec0, ebase + 4) + enum = getint(rec0, ebase + 8) rlen = len(rec0) return ebase, elen, enum, rlen def add_exth(rec0, exth_num, exth_bytes): ebase, elen, enum, rlen = get_exth_params(rec0) - newrecsize = 8+len(exth_bytes) - newrec0 = rec0[0:ebase+4]+struct.pack('>L', elen+newrecsize)+struct.pack('>L', enum+1)+struct.pack('>L', exth_num)\ - + struct.pack('>L', newrecsize)+exth_bytes+rec0[ebase+12:] - newrec0 = writeint(newrec0, title_offset, getint(newrec0, title_offset)+newrecsize) + newrecsize = 8 + len(exth_bytes) + newrec0 = rec0[0:ebase + 4] + struct.pack('>L', elen + newrecsize) + struct.pack('>L', enum + 1) + \ + struct.pack('>L', exth_num) + struct.pack('>L', newrecsize) + exth_bytes + rec0[ebase + 12:] + newrec0 = writeint(newrec0, title_offset, getint(newrec0, title_offset) + newrecsize) # keep constant record length by removing newrecsize null bytes from end sectail = newrec0[-newrecsize:] - if sectail != b'\0'*newrecsize: + if sectail != b'\0' * newrecsize: raise DualMetaFixException('add_exth: trimmed non-null bytes at end of section') newrec0 = newrec0[0:rlen] return newrec0 @@ -106,30 +106,31 @@ def read_exth(rec0, exth_num): exth_id = getint(rec0, ebase) if exth_id == exth_num: # We might have multiple exths, so build a list. - exth_values.append(rec0[ebase+8:ebase+getint(rec0, ebase+4)]) + exth_values.append(rec0[ebase + 8:ebase + getint(rec0, ebase + 4)]) enum -= 1 - ebase = ebase+getint(rec0, ebase+4) + ebase = ebase + getint(rec0, ebase + 4) return exth_values def del_exth(rec0, exth_num): ebase, elen, enum, rlen = get_exth_params(rec0) - ebase_idx = ebase+12 + ebase_idx = ebase + 12 enum_idx = 0 while enum_idx < enum: exth_id = getint(rec0, ebase_idx) - exth_size = getint(rec0, ebase_idx+4) + exth_size = getint(rec0, ebase_idx + 4) if exth_id == exth_num: newrec0 = rec0 - newrec0 = writeint(newrec0, title_offset, getint(newrec0, title_offset)-exth_size) - newrec0 = newrec0[:ebase_idx]+newrec0[ebase_idx+exth_size:] - newrec0 = newrec0[0:ebase+4]+struct.pack('>L', elen-exth_size)+struct.pack('>L', enum-1)+newrec0[ebase+12:] - newrec0 += b'\0'*exth_size + newrec0 = writeint(newrec0, title_offset, getint(newrec0, title_offset) - exth_size) + newrec0 = newrec0[:ebase_idx] + newrec0[ebase_idx + exth_size:] + newrec0 = newrec0[0:ebase + 4] + struct.pack('>L', elen - exth_size) + \ + struct.pack('>L', enum - 1) + newrec0[ebase + 12:] + newrec0 += b'\0' * exth_size if rlen != len(newrec0): raise DualMetaFixException('del_exth: incorrect section size change') return newrec0 enum_idx += 1 - ebase_idx = ebase_idx+exth_size + ebase_idx = ebase_idx + exth_size return rec0 diff --git a/kcc/image.py b/kcc/image.py index cd0213b..b98bc2a 100755 --- a/kcc/image.py +++ b/kcc/image.py @@ -148,8 +148,8 @@ class ComicPage: if self.noVPV: flags.append('NoVerticalPanelView') if self.border: - flags.append('Margins-' + str(self.border[0]) + '-' + str(self.border[1]) + '-' - + str(self.border[2]) + '-' + str(self.border[3])) + flags.append('Margins-' + str(self.border[0]) + '-' + str(self.border[1]) + '-' + + str(self.border[2]) + '-' + str(self.border[3])) if self.fill != 'white': flags.append('BlackFill') if self.opt.quality == 2: @@ -199,10 +199,10 @@ class ComicPage: else: multiplier = 1.5 if border is not None: - self.border = [round(float(border[0])/float(self.image.size[0])*150, 3), - round(float(border[1])/float(self.image.size[1])*150, 3), - round(float(self.image.size[0]-border[2])/float(self.image.size[0])*150, 3), - round(float(self.image.size[1]-border[3])/float(self.image.size[1])*150, 3)] + self.border = [round(float(border[0]) / float(self.image.size[0]) * 150, 3), + round(float(border[1]) / float(self.image.size[1]) * 150, 3), + round(float(self.image.size[0] - border[2]) / float(self.image.size[0]) * 150, 3), + round(float(self.image.size[1] - border[3]) / float(self.image.size[1]) * 150, 3)] if int((border[2] - border[0]) * multiplier) < self.size[0] + 10: self.noHPV = True if int((border[3] - border[1]) * multiplier) < self.size[1] + 10: @@ -428,13 +428,13 @@ class ComicPage: while startY < bw.size[1]: if startY + 5 > bw.size[1]: startY = bw.size[1] - 5 - fill += self.getImageHistogram(bw.crop((0, startY, bw.size[0], startY+5))) + fill += self.getImageHistogram(bw.crop((0, startY, bw.size[0], startY + 5))) startY += 5 startX = 0 while startX < bw.size[0]: if startX + 5 > bw.size[0]: startX = bw.size[0] - 5 - fill += self.getImageHistogram(bw.crop((startX, 0, startX+5, bw.size[1]))) + fill += self.getImageHistogram(bw.crop((startX, 0, startX + 5, bw.size[1]))) startX += 5 if fill > 0: self.fill = 'black' @@ -442,29 +442,25 @@ class ComicPage: self.fill = 'white' def isImageColor(self): - v = ImageStat.Stat(self.image).var - isMonochromatic = reduce(lambda x, y: x and y < 0.005, v, True) - if isMonochromatic: - # Monochromatic - return False - else: - if len(v) == 3: - maxmin = abs(max(v) - min(v)) - if maxmin > 1000: - # Color - return True - elif maxmin > 100: - # Probably color - return True - else: - # Grayscale - return False - elif len(v) == 1: - # Black and white + img = self.image.copy() + bands = img.getbands() + if bands == ('R', 'G', 'B') or bands == ('R', 'G', 'B', 'A'): + thumb = img.resize((40, 40)) + SSE, bias = 0, [0, 0, 0] + bias = ImageStat.Stat(thumb).mean[:3] + bias = [b - sum(bias) / 3 for b in bias] + for pixel in thumb.getdata(): + mu = sum(pixel) / 3 + SSE += sum((pixel[i] - mu - bias[i]) * (pixel[i] - mu - bias[i]) for i in [0, 1, 2]) + MSE = float(SSE) / (40 * 40) + if MSE <= 22: return False else: - # Detection failed - return False + return True + elif len(bands) == 1: + return False + else: + return False class Cover: @@ -513,4 +509,4 @@ class Cover: try: self.image.save(self.target, "JPEG", optimize=1, quality=80) except IOError: - raise RuntimeError('Failed to save cover') + raise RuntimeError('Failed to process downloaded cover.') diff --git a/kcc/rarfile.py b/kcc/rarfile.py index 567f5ce..afb19a7 100644 --- a/kcc/rarfile.py +++ b/kcc/rarfile.py @@ -360,9 +360,8 @@ class RarCannotExec(RarExecError): def is_rarfile(xfile): '''Check quickly whether file is rar archive.''' - fd = XFile(xfile) - buf = fd.read(len(RAR_ID)) - fd.close() + with open(xfile, 'rb') as fh: + buf = fh.read(len(RAR_ID)) if buf == RAR_ID or buf == RAR5_ID: return True else: diff --git a/kcc/shared.py b/kcc/shared.py index 941d9b5..eac6bf8 100644 --- a/kcc/shared.py +++ b/kcc/shared.py @@ -25,6 +25,7 @@ from shutil import rmtree, move from tempfile import mkdtemp from zipfile import ZipFile, ZIP_DEFLATED from re import split +from traceback import format_tb try: from scandir import walk except ImportError: @@ -45,6 +46,9 @@ class HTMLStripper(HTMLParser): def get_data(self): return ''.join(self.fed) + def error(self, message): + pass + def getImageFileName(imgfile): name, ext = os.path.splitext(imgfile) @@ -117,16 +121,22 @@ def removeFromZIP(zipfname, *filenames): rmtree(tempdir) -# noinspection PyUnresolvedReferences +def sanitizeTrace(traceback): + return ''.join(format_tb(traceback))\ + .replace('C:\\Users\\pawel\\Documents\\Projekty\\KCC\\', '')\ + .replace('C:\\Python34\\', '')\ + .replace('C:\\Python34_64\\', '') + + def dependencyCheck(level): missing = [] if level > 2: try: from PyQt5.QtCore import qVersion as qtVersion - if StrictVersion('5.2.0') > StrictVersion(qtVersion()): - missing.append('PyQt 5.2.0+') + if StrictVersion('5.4.0') > StrictVersion(qtVersion()): + missing.append('PyQt 5.4.0+') except ImportError: - missing.append('PyQt 5.2.0+') + missing.append('PyQt 5.4.0+') if level > 1: try: from psutil import __version__ as psutilVersion @@ -136,10 +146,10 @@ def dependencyCheck(level): missing.append('psutil 3.0.0+') try: from slugify import __version__ as slugifyVersion - if StrictVersion('1.1.2') > StrictVersion(slugifyVersion): - missing.append('python-slugify 1.1.2+') + if StrictVersion('1.1.3') > StrictVersion(slugifyVersion): + missing.append('python-slugify 1.1.3+') except ImportError: - missing.append('python-slugify 1.1.2+') + missing.append('python-slugify 1.1.3+') try: from PIL import PILLOW_VERSION as pillowVersion if StrictVersion('2.8.2') > StrictVersion(pillowVersion): diff --git a/setup.json b/setup.json new file mode 100644 index 0000000..456786a --- /dev/null +++ b/setup.json @@ -0,0 +1,10 @@ +{ + "title": "Kindle Comic Converter", + "icon": "icons/comic2ebook.icns", + "background": "icons/WizardOSX.png", + "icon-size": 160, + "contents": [ + { "x": 180, "y": 300, "type": "file", "path": "dist/Kindle Comic Converter.app" }, + { "x": 520, "y": 300, "type": "link", "path": "/Applications" } + ] +} diff --git a/setup.py b/setup.py index 95b712c..8a7c8cc 100755 --- a/setup.py +++ b/setup.py @@ -22,10 +22,9 @@ VERSION = __version__ MAIN = 'kcc.py' extra_options = {} -# noinspection PyUnresolvedReferences if platform == 'darwin': from setuptools import setup - from os import chmod, makedirs + from os import chmod, makedirs, system from shutil import copyfile extra_options = dict( setup_requires=['py2app'], @@ -34,10 +33,10 @@ if platform == 'darwin': py2app=dict( argv_emulation=True, iconfile='icons/comic2ebook.icns', - includes=['sip', 'PyQt5.QtPrintSupport'], + includes=['sip'], resources=['LICENSE.txt', 'other/qt.conf', 'other/Additional-LICENSE.txt', 'other/unrar', 'other/7za'], plist=dict( - CFBundleName=NAME, + CFBundleName='Kindle Comic Converter', CFBundleShortVersionString=VERSION, CFBundleGetInfoString=NAME + ' ' + VERSION + ', written 2012-2015 by Ciro Mattia Gonano and Pawel Jastrzebski', @@ -60,7 +59,6 @@ if platform == 'darwin': ) ) elif platform == 'win32': - # noinspection PyUnresolvedReferences import py2exe from platform import architecture from distutils.core import setup @@ -137,7 +135,7 @@ else: install_requires=[ 'Pillow>=2.8.2', 'psutil>=3.0.0', - 'python-slugify>=1.1.2', + 'python-slugify>=1.1.3', 'scandir>=1.1.0', ], zip_safe=False, @@ -156,7 +154,8 @@ setup( ) if platform == 'darwin': - makedirs('dist/' + NAME + '.app/Contents/PlugIns/platforms', exist_ok=True) - copyfile('other/libqcocoa.dylib', 'dist/' + NAME + '.app/Contents/PlugIns/platforms/libqcocoa.dylib') - chmod('dist/' + NAME + '.app/Contents/Resources/unrar', 0o777) - chmod('dist/' + NAME + '.app/Contents/Resources/7za', 0o777) + makedirs('dist/Kindle Comic Converter.app/Contents/PlugIns/platforms', exist_ok=True) + copyfile('other/libqcocoa.dylib', 'dist/Kindle Comic Converter.app/Contents/PlugIns/platforms/libqcocoa.dylib') + chmod('dist/Kindle Comic Converter.app/Contents/Resources/unrar', 0o777) + chmod('dist/Kindle Comic Converter.app/Contents/Resources/7za', 0o777) + system('appdmg setup.json dist/KindleComicConverter_osx_' + VERSION + '.dmg') |