1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-18 06:58:58 +00:00

Compare commits

..

25 Commits
4.6.1 ... 4.6.4

Author SHA1 Message Date
Paweł Jastrzębski
eab63a0f74 Merge pull request #153 from ciromattia/dev
4.6.4
2015-09-11 09:38:56 +02:00
Paweł Jastrzębski
2128104db7 Updated README + version bump 2015-09-11 09:38:28 +02:00
Paweł Jastrzębski
c6179b0064 Tweaked CBZ detection 2015-09-06 18:26:15 +02:00
Paweł Jastrzębski
1d4319be2e CLI: Additional source path cleanup (close #152) 2015-09-06 14:49:56 +02:00
Paweł Jastrzębski
f5a738e2d4 Updated OSX release to QT 5.5 2015-09-06 11:23:45 +02:00
Paweł Jastrzębski
477d834a91 Updated OSX installer 2015-09-05 09:14:26 +02:00
Paweł Jastrzębski
c8698f6d99 Improved error handling 2015-09-04 18:40:02 +02:00
Paweł Jastrzębski
0988601842 Implemented new method to detect color images 2015-09-04 16:06:18 +02:00
Paweł Jastrzębski
57e9637c81 Code cleanup 2015-09-03 17:13:46 +02:00
Paweł Jastrzębski
a7440e06a9 Additional temp cleanup 2015-09-01 18:53:09 +02:00
Paweł Jastrzębski
a9ed1e7610 Improved error reporting 2015-09-01 18:15:33 +02:00
Paweł Jastrzębski
b1bc140ad3 Reversed OS X version to Qt 4.9.2 2015-08-29 09:43:52 +02:00
Paweł Jastrzębski
9014ed53d4 Merge pull request #151 from ciromattia/dev
4.6.3
2015-08-28 19:53:59 +02:00
Paweł Jastrzębski
cad05904f3 Updated README + version bump 2015-08-28 19:53:16 +02:00
Paweł Jastrzębski
10386d8af3 Added detailed platform info to error report 2015-08-28 19:50:43 +02:00
Paweł Jastrzębski
c991feb9ce GUI tweaks 2015-08-28 19:33:40 +02:00
Paweł Jastrzębski
d26eb7cdcd Set proper User-Agent 2015-08-24 17:18:39 +02:00
Paweł Jastrzębski
351084b703 Implemented error reporting 2015-08-24 17:11:35 +02:00
Paweł Jastrzębski
e861e7f6e8 Updated page endpoints 2015-08-24 17:07:49 +02:00
Paweł Jastrzębski
370c9d4df7 Tweaked setup.py 2015-08-09 10:08:24 +02:00
Paweł Jastrzębski
8e5704683c Fixed detection of file corruption 2015-08-04 09:54:39 +02:00
Paweł Jastrzębski
c65e1c8dea Merge pull request #150 from ciromattia/dev
4.6.2
2015-07-14 18:01:51 +02:00
Paweł Jastrzębski
677622c103 Updated README + version bump 2015-07-14 18:01:02 +02:00
Paweł Jastrzębski
af0ebb85a0 Escape HTML in metadata (close #148) 2015-07-14 17:58:59 +02:00
Paweł Jastrzębski
8af029ac92 Fixed MOBI header (close #149) 2015-07-14 17:40:33 +02:00
20 changed files with 275 additions and 177 deletions

View File

@@ -32,10 +32,10 @@ You can find the latest released binary at the following links:
## DEPENDENCIES ## DEPENDENCIES
Following software is required to run Linux version of **KCC** and/or bare sources: Following software is required to run Linux version of **KCC** and/or bare sources:
- Python 3.3+ - 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+ - [Pillow](http://pypi.python.org/pypi/Pillow/) 2.8.2+
- [psutil](https://pypi.python.org/pypi/psutil) 3.0.0+ - [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+ - [scandir](https://pypi.python.org/pypi/scandir) 1.1.0+
On Debian based distributions these two commands should install all needed dependencies: On Debian based distributions these two commands should install all needed dependencies:
@@ -156,6 +156,20 @@ The app relies and includes the following scripts:
* [Kobo Aura H2O](http://kcc.iosphe.re/Samples/Ubunchu-KoAH2O.kepub.epub) * [Kobo Aura H2O](http://kcc.iosphe.re/Samples/Ubunchu-KoAH2O.kepub.epub)
## CHANGELOG ## 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
####4.6.2:
* Fixed critical MOBI header bug
* Fixed metadata encoding error
####4.6.1: ####4.6.1:
* Fixed KEPUB TOC generator * Fixed KEPUB TOC generator
* Added warning about too small input files * Added warning about too small input files
@@ -419,6 +433,14 @@ The app relies and includes the following scripts:
####1.0 ####1.0
* Initial version * Initial version
## PRIVACY
**KCC** is initiating internet connections in three cases:
* During startup - Version check
* When MCD metadata are used - Cover download
* When error occurs - Automatic reporting
Error report include **KCC** version, OS version and content of error message.
## KNOWN ISSUES ## KNOWN ISSUES
Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues). Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues).

View File

@@ -462,6 +462,12 @@
<property name="selectionMode"> <property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum> <enum>QAbstractItemView::NoSelection</enum>
</property> </property>
<property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
<property name="horizontalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
</widget> </widget>
<widget class="QPushButton" name="BasicModeButton"> <widget class="QPushButton" name="BasicModeButton">
<property name="geometry"> <property name="geometry">

View File

@@ -397,6 +397,12 @@
<property name="selectionMode"> <property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum> <enum>QAbstractItemView::NoSelection</enum>
</property> </property>
<property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
<property name="horizontalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
</widget> </widget>
<widget class="QPushButton" name="BasicModeButton"> <widget class="QPushButton" name="BasicModeButton">
<property name="geometry"> <property name="geometry">

View File

@@ -126,6 +126,9 @@
</rect> </rect>
</property> </property>
<layout class="QFormLayout" name="formLayout"> <layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">

BIN
icons/WizardOSX.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

View File

@@ -1,5 +1,5 @@
#define MyAppName "Kindle Comic Converter" #define MyAppName "Kindle Comic Converter"
#define MyAppVersion "4.6.1" #define MyAppVersion "4.6.4"
#define MyAppPublisher "Ciro Mattia Gonano, Paweł Jastrzębski" #define MyAppPublisher "Ciro Mattia Gonano, Paweł Jastrzębski"
#define MyAppURL "http://kcc.iosphe.re/" #define MyAppURL "http://kcc.iosphe.re/"
#define MyAppExeName "KCC.exe" #define MyAppExeName "KCC.exe"

View File

@@ -76,6 +76,7 @@ class Ui_MetaEditorDialog(object):
self.formLayoutWidget.setObjectName("formLayoutWidget") self.formLayoutWidget.setObjectName("formLayoutWidget")
self.formLayout = QtWidgets.QFormLayout(self.formLayoutWidget) self.formLayout = QtWidgets.QFormLayout(self.formLayoutWidget)
self.formLayout.setContentsMargins(0, 0, 0, 0) self.formLayout.setContentsMargins(0, 0, 0, 0)
self.formLayout.setFieldGrowthPolicy(QtWidgets.QFormLayout.ExpandingFieldsGrow)
self.formLayout.setObjectName("formLayout") self.formLayout.setObjectName("formLayout")
self.label = QtWidgets.QLabel(self.formLayoutWidget) self.label = QtWidgets.QLabel(self.formLayoutWidget)
self.label.setObjectName("label") self.label.setObjectName("label")

View File

@@ -20,21 +20,22 @@
import os import os
import sys import sys
from urllib.parse import unquote from urllib.parse import unquote
from urllib.request import urlopen, urlretrieve from urllib.request import urlopen, urlretrieve, Request
from socket import gethostbyname_ex, gethostname from socket import gethostbyname_ex, gethostname
from traceback import format_tb from time import sleep, time
from time import sleep from datetime import datetime
from shutil import move from shutil import move
from http.server import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn from socketserver import ThreadingMixIn
from subprocess import STDOUT, PIPE from subprocess import STDOUT, PIPE
from PyQt5 import QtGui, QtCore, QtWidgets, QtNetwork from PyQt5 import QtGui, QtCore, QtWidgets, QtNetwork
from xml.dom.minidom import parse from xml.dom.minidom import parse, Document
from psutil import Popen, Process from psutil import Popen, Process
from copy import copy from copy import copy
from distutils.version import StrictVersion from distutils.version import StrictVersion
from xml.sax.saxutils import escape from xml.sax.saxutils import escape
from .shared import md5Checksum, HTMLStripper from platform import platform
from .shared import md5Checksum, HTMLStripper, sanitizeTrace
from . import __version__ from . import __version__
from . import comic2ebook from . import comic2ebook
from . import KCC_rc_web from . import KCC_rc_web
@@ -65,7 +66,6 @@ class QApplicationMessaging(QtWidgets.QApplication):
socket.connectToServer(self._key, QtCore.QIODevice.WriteOnly) socket.connectToServer(self._key, QtCore.QIODevice.WriteOnly)
if not socket.waitForConnected(self._timeout): if not socket.waitForConnected(self._timeout):
self._server = QtNetwork.QLocalServer(self) self._server = QtNetwork.QLocalServer(self)
# noinspection PyUnresolvedReferences
self._server.newConnection.connect(self.handleMessage) self._server.newConnection.connect(self.handleMessage)
self._server.listen(self._key) self._server.listen(self._key)
else: else:
@@ -138,7 +138,7 @@ class Icons:
class WebServerHandler(BaseHTTPRequestHandler): class WebServerHandler(BaseHTTPRequestHandler):
# noinspection PyAttributeOutsideInit, PyArgumentList # noinspection PyAttributeOutsideInit
def do_GET(self): def do_GET(self):
if self.path == '/': if self.path == '/':
self.path = '/index.html' self.path = '/index.html'
@@ -245,22 +245,22 @@ class VersionThread(QtCore.QThread):
def run(self): def run(self):
try: try:
XML = urlopen('http://kcc.iosphe.re/Version.php') XML = parse(urlopen(Request('https://kcc.iosphe.re/Version/',
XML = parse(XML) headers={'User-Agent': 'KindleComicConverter/' + __version__})))
except Exception: except Exception:
return return
latestVersion = XML.childNodes[0].getElementsByTagName('latest')[0].childNodes[0].toxml() latestVersion = XML.childNodes[0].getElementsByTagName('LatestVersion')[0].childNodes[0].toxml()
if StrictVersion(latestVersion) > StrictVersion(__version__): if StrictVersion(latestVersion) > StrictVersion(__version__):
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
self.newVersion = latestVersion self.newVersion = latestVersion
self.md5 = XML.childNodes[0].getElementsByTagName('WindowsMD5')[0].childNodes[0].toxml() self.md5 = XML.childNodes[0].getElementsByTagName('MD5')[0].childNodes[0].toxml()
MW.showDialog.emit('<b>New version released!</b> <a href="https://github.com/ciromattia/kcc/releases/">' MW.showDialog.emit('<b>New version released!</b> <a href="https://github.com/ciromattia/kcc/releases/">'
'See changelog.</a><br/><br/>Installed version: ' + __version__ + 'See changelog.</a><br/><br/>Installed version: ' + __version__ +
'<br/>Current version: ' + latestVersion + '<br/>Current version: ' + latestVersion +
'<br/><br/>Would you like to start automatic update?', 'question') '<br/><br/>Would you like to start automatic update?', 'question')
self.getNewVersion() self.getNewVersion()
else: else:
MW.addMessage.emit('<a href="http://kcc.iosphe.re/">' MW.addMessage.emit('<a href="https://kcc.iosphe.re/">'
'<b>New version is available!</b></a> ' '<b>New version is available!</b></a> '
'(<a href="https://github.com/ciromattia/kcc/releases/">' '(<a href="https://github.com/ciromattia/kcc/releases/">'
'Changelog</a>)', 'warning', False) 'Changelog</a>)', 'warning', False)
@@ -275,8 +275,8 @@ class VersionThread(QtCore.QThread):
try: try:
MW.modeConvert.emit(-1) MW.modeConvert.emit(-1)
MW.progressBarTick.emit('Downloading update') MW.progressBarTick.emit('Downloading update')
path = urlretrieve('http://kcc.iosphe.re/Windows/KindleComicConverter_win_' path = urlretrieve('https://kcc.iosphe.re/Windows/KindleComicConverter_win_' +
+ self.newVersion + '.exe', reporthook=self.getNewVersionTick) self.newVersion + '.exe', reporthook=self.getNewVersionTick)
if self.md5 != md5Checksum(path[0]): if self.md5 != md5Checksum(path[0]):
raise Exception raise Exception
move(path[0], path[0] + '.exe') move(path[0], path[0] + '.exe')
@@ -323,7 +323,6 @@ class ProgressThread(QtCore.QThread):
class WorkerThread(QtCore.QThread): class WorkerThread(QtCore.QThread):
# noinspection PyArgumentList
def __init__(self): def __init__(self):
QtCore.QThread.__init__(self) QtCore.QThread.__init__(self)
self.conversionAlive = False self.conversionAlive = False
@@ -348,12 +347,6 @@ class WorkerThread(QtCore.QThread):
MW.addTrayMessage.emit('Conversion interrupted.', 'Critical') MW.addTrayMessage.emit('Conversion interrupted.', 'Critical')
MW.modeConvert.emit(1) MW.modeConvert.emit(1)
def sanitizeTrace(self, traceback):
return ''.join(format_tb(traceback))\
.replace('C:\\Users\\AcidWeb\\Documents\\Projekty\\KCC\\', '')\
.replace('C:\\Python34\\', '')\
.replace('C:\\Python34_64\\', '')
def run(self): def run(self):
MW.modeConvert.emit(0) MW.modeConvert.emit(0)
@@ -437,16 +430,20 @@ class WorkerThread(QtCore.QThread):
GUI.progress.content = '' GUI.progress.content = ''
self.errors = True self.errors = True
MW.addMessage.emit(str(warn), 'warning', False) MW.addMessage.emit(str(warn), 'warning', False)
MW.addMessage.emit('Failed to create output file!', 'error', False) MW.addMessage.emit('Error during conversion! Please consult '
MW.addTrayMessage.emit('Failed to create output file!', 'Critical') '<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: except Exception as err:
GUI.progress.content = '' GUI.progress.content = ''
self.errors = True self.errors = True
_, _, traceback = sys.exc_info() _, _, traceback = sys.exc_info()
MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s" MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s"
% (jobargv[-1], str(err), self.sanitizeTrace(traceback)), 'error') % (jobargv[-1], str(err), sanitizeTrace(traceback)), 'error')
MW.addMessage.emit('Failed to create EPUB!', 'error', False) MW.addMessage.emit('Error during conversion! Please consult '
MW.addTrayMessage.emit('Failed to create EPUB!', 'Critical') '<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: if not self.conversionAlive:
for item in outputPath: for item in outputPath:
if os.path.exists(item): if os.path.exists(item):
@@ -461,7 +458,7 @@ class WorkerThread(QtCore.QThread):
MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True) MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True)
if str(GUI.FormatBox.currentText()) == 'MOBI': if str(GUI.FormatBox.currentText()) == 'MOBI':
MW.progressBarTick.emit('Creating MOBI files') 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.progressBarTick.emit('tick')
MW.addMessage.emit('Creating MOBI files', 'info', False) MW.addMessage.emit('Creating MOBI files', 'info', False)
GUI.progress.content = 'Creating MOBI files' GUI.progress.content = 'Creating MOBI files'
@@ -500,7 +497,7 @@ class WorkerThread(QtCore.QThread):
GUI.progress.content = '' GUI.progress.content = ''
mobiPath = item.replace('.epub', '.mobi') mobiPath = item.replace('.epub', '.mobi')
os.remove(mobiPath + '_toclean') 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: try:
move(mobiPath, GUI.targetDirectory) move(mobiPath, GUI.targetDirectory)
mobiPath = os.path.join(GUI.targetDirectory, os.path.basename(mobiPath)) mobiPath = os.path.join(GUI.targetDirectory, os.path.basename(mobiPath))
@@ -520,7 +517,7 @@ class WorkerThread(QtCore.QThread):
MW.addTrayMessage.emit('Failed to process MOBI file!', 'Critical') MW.addTrayMessage.emit('Failed to process MOBI file!', 'Critical')
else: else:
GUI.progress.content = '' 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: for item in outputPath:
if os.path.exists(item): if os.path.exists(item):
os.remove(item) os.remove(item)
@@ -536,7 +533,7 @@ class WorkerThread(QtCore.QThread):
False) False)
else: else:
for item in outputPath: 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: try:
move(item, GUI.targetDirectory) move(item, GUI.targetDirectory)
item = os.path.join(GUI.targetDirectory, os.path.basename(item)) item = os.path.join(GUI.targetDirectory, os.path.basename(item))
@@ -547,8 +544,9 @@ class WorkerThread(QtCore.QThread):
GUI.progress.stop() GUI.progress.stop()
MW.hideProgressBar.emit() MW.hideProgressBar.emit()
GUI.needClean = True GUI.needClean = True
MW.addMessage.emit('<b>All jobs completed.</b>', 'info', False) if not self.errors:
MW.addTrayMessage.emit('All jobs completed.', 'Information') MW.addMessage.emit('<b>All jobs completed.</b>', 'info', False)
MW.addTrayMessage.emit('All jobs completed.', 'Information')
MW.modeConvert.emit(1) MW.modeConvert.emit(1)
@@ -557,7 +555,6 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
super().__init__() super().__init__()
if self.isSystemTrayAvailable(): if self.isSystemTrayAvailable():
QtWidgets.QSystemTrayIcon.__init__(self, GUI.icons.programIcon, MW) QtWidgets.QSystemTrayIcon.__init__(self, GUI.icons.programIcon, MW)
# noinspection PyUnresolvedReferences
self.activated.connect(self.catchClicks) self.activated.connect(self.catchClicks)
def catchClicks(self): def catchClicks(self):
@@ -633,8 +630,10 @@ class KCCGUI(KCC_ui.Ui_KCC):
self.lastPath = os.path.abspath(os.path.join(fname, os.pardir)) self.lastPath = os.path.abspath(os.path.join(fname, os.pardir))
try: try:
self.editor.loadData(fname) self.editor.loadData(fname)
except: except Exception as err:
self.showDialog('Failed to parse metadata!', 'error') _, _, traceback = sys.exc_info()
self.showDialog("Failed to parse metadata!\n\n%s\n\nTraceback:\n%s"
% (str(err), sanitizeTrace(traceback)), 'error')
else: else:
self.editor.ui.exec_() self.editor.ui.exec_()
@@ -843,7 +842,7 @@ class KCCGUI(KCC_ui.Ui_KCC):
def changeGamma(self, value): def changeGamma(self, value):
value = float(value) value = float(value)
value = '%.2f' % (value/100) value = '%.2f' % (value / 100)
if float(value) <= 0.09: if float(value) <= 0.09:
GUI.GammaLabel.setText('Gamma: Auto') GUI.GammaLabel.setText('Gamma: Auto')
else: else:
@@ -911,7 +910,7 @@ class KCCGUI(KCC_ui.Ui_KCC):
else: else:
item = QtWidgets.QListWidgetItem(' ' + self.stripTags(message)) item = QtWidgets.QListWidgetItem(' ' + self.stripTags(message))
if replace: 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 # 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 # We still fill original text field with transparent content to trigger creation of horizontal scrollbar
item.setForeground(QtGui.QColor('transparent')) item.setForeground(QtGui.QColor('transparent'))
@@ -928,6 +927,31 @@ class KCCGUI(KCC_ui.Ui_KCC):
def showDialog(self, message, kind): def showDialog(self, message, kind):
if kind == 'error': if kind == 'error':
QtWidgets.QMessageBox.critical(MW, 'KCC - Error', message, QtWidgets.QMessageBox.Ok) QtWidgets.QMessageBox.critical(MW, 'KCC - Error', message, QtWidgets.QMessageBox.Ok)
try:
doc = Document()
root = doc.createElement('KCCErrorReport')
doc.appendChild(root)
main = doc.createElement('Timestamp')
root.appendChild(main)
text = doc.createTextNode(datetime.fromtimestamp(time()).strftime('%Y-%m-%d %H:%M:%S'))
main.appendChild(text)
main = doc.createElement('OS')
root.appendChild(main)
text = doc.createTextNode(platform())
main.appendChild(text)
main = doc.createElement('Version')
root.appendChild(main)
text = doc.createTextNode(__version__)
main.appendChild(text)
main = doc.createElement('Error')
root.appendChild(main)
text = doc.createTextNode(message)
main.appendChild(text)
urlopen(Request(url='https://kcc.iosphe.re/ErrorHandle/', data=doc.toxml(encoding='utf-8'),
headers={'Content-Type': 'application/xml',
'User-Agent': 'KindleComicConverter/' + __version__}))
except:
pass
elif kind == 'question': elif kind == 'question':
GUI.versionCheck.setAnswer(QtWidgets.QMessageBox.question(MW, 'KCC - Question', message, GUI.versionCheck.setAnswer(QtWidgets.QMessageBox.question(MW, 'KCC - Question', message,
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.Yes,
@@ -1026,7 +1050,7 @@ class KCCGUI(KCC_ui.Ui_KCC):
'ColorBox': GUI.ColorBox.checkState(), 'ColorBox': GUI.ColorBox.checkState(),
'customWidth': GUI.customWidth.text(), 'customWidth': GUI.customWidth.text(),
'customHeight': GUI.customHeight.text(), 'customHeight': GUI.customHeight.text(),
'GammaSlider': float(self.GammaValue)*100}) 'GammaSlider': float(self.GammaValue) * 100})
self.settings.sync() self.settings.sync()
self.tray.hide() self.tray.hide()
@@ -1205,7 +1229,7 @@ class KCCGUI(KCC_ui.Ui_KCC):
"Kindle DX/DXG", "Kindle DX/DXG",
] ]
statusBarLabel = QtWidgets.QLabel('<b><a href="http://kcc.iosphe.re/">HOMEPAGE</a> - <a href="https://github.' statusBarLabel = QtWidgets.QLabel('<b><a href="https://kcc.iosphe.re/">HOMEPAGE</a> - <a href="https://github.'
'com/ciromattia/kcc/blob/master/README.md#issues--new-features--donations">DO' 'com/ciromattia/kcc/blob/master/README.md#issues--new-features--donations">DO'
'NATE</a> - <a href="https://github.com/ciromattia/kcc/wiki">WIKI</a> - <a hr' 'NATE</a> - <a href="https://github.com/ciromattia/kcc/wiki">WIKI</a> - <a hr'
'ef="http://www.mobileread.com/forums/showthread.php?t=207461">FORUM</a></b>') 'ef="http://www.mobileread.com/forums/showthread.php?t=207461">FORUM</a></b>')
@@ -1273,7 +1297,7 @@ class KCCGUI(KCC_ui.Ui_KCC):
if profile == "Other": if profile == "Other":
GUI.DeviceBox.addItem(self.icons.deviceOther, profile) GUI.DeviceBox.addItem(self.icons.deviceOther, profile)
elif profile == "Separator": elif profile == "Separator":
GUI.DeviceBox.insertSeparator(GUI.DeviceBox.count()+1) GUI.DeviceBox.insertSeparator(GUI.DeviceBox.count() + 1)
elif 'Ko' in profile: elif 'Ko' in profile:
GUI.DeviceBox.addItem(self.icons.deviceKobo, profile) GUI.DeviceBox.addItem(self.icons.deviceKobo, profile)
else: else:
@@ -1307,6 +1331,12 @@ class KCCGUI(KCC_ui.Ui_KCC):
self.versionCheck.start() self.versionCheck.start()
self.contentServer.start() self.contentServer.start()
self.tray.show() self.tray.show()
# Linux hack as PyQt 5.5 not hit mainstream distributions yet
if sys.platform.startswith('linux') and StrictVersion(QtCore.qVersion()) > StrictVersion('5.4.9'):
self.JobList.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
self.JobList.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
MW.setWindowTitle("Kindle Comic Converter " + __version__) MW.setWindowTitle("Kindle Comic Converter " + __version__)
MW.show() MW.show()
MW.raise_() MW.raise_()
@@ -1348,8 +1378,10 @@ class KCCGUI_MetaEditor(KCC_MetaEditor_ui.Ui_MetaEditorDialog):
self.parser.data[field.objectName()[:-4] + 's'] = tmpData self.parser.data[field.objectName()[:-4] + 's'] = tmpData
try: try:
self.parser.saveXML() self.parser.saveXML()
except: except Exception as err:
GUI.showDialog('Failed to save metadata!', 'error') _, _, 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() self.ui.close()
def cleanData(self, s): def cleanData(self, s):

View File

@@ -141,6 +141,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.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.setProperty("showDropIndicator", False)
self.JobList.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) self.JobList.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
self.JobList.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
self.JobList.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
self.JobList.setObjectName("JobList") self.JobList.setObjectName("JobList")
self.BasicModeButton = QtWidgets.QPushButton(self.Form) self.BasicModeButton = QtWidgets.QPushButton(self.Form)
self.BasicModeButton.setGeometry(QtCore.QRect(10, 10, 141, 32)) self.BasicModeButton.setGeometry(QtCore.QRect(10, 10, 141, 32))

View File

@@ -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.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.setProperty("showDropIndicator", False)
self.JobList.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) self.JobList.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
self.JobList.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
self.JobList.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
self.JobList.setObjectName("JobList") self.JobList.setObjectName("JobList")
self.BasicModeButton = QtWidgets.QPushButton(self.Form) self.BasicModeButton = QtWidgets.QPushButton(self.Form)
self.BasicModeButton.setGeometry(QtCore.QRect(5, 10, 156, 41)) self.BasicModeButton.setGeometry(QtCore.QRect(5, 10, 156, 41))

View File

@@ -1,4 +1,4 @@
__version__ = '4.6.1' __version__ = '4.6.4'
__license__ = 'ISC' __license__ = 'ISC'
__copyright__ = '2012-2015, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>' __copyright__ = '2012-2015, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'

View File

@@ -70,8 +70,8 @@ class CBxArchive:
if sys.platform.startswith('darwin'): if sys.platform.startswith('darwin'):
copy(self.origFileName, os.path.join(os.path.dirname(self.origFileName), 'TMP_KCC_TMP')) 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') 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"' 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) targetdir + '"', stdout=PIPE, stderr=STDOUT, shell=True)
extracted = False extracted = False
for line in output.stdout: for line in output.stdout:
if b"Everything is Ok" in line: if b"Everything is Ok" in line:

View File

@@ -28,7 +28,7 @@ from urllib.request import Request, urlopen
from re import sub from re import sub
from stat import S_IWRITE, S_IREAD, S_IEXEC from stat import S_IWRITE, S_IREAD, S_IEXEC
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
from tempfile import mkdtemp from tempfile import mkdtemp, gettempdir
from shutil import move, copytree, rmtree from shutil import move, copytree, rmtree
from optparse import OptionParser, OptionGroup from optparse import OptionParser, OptionGroup
from multiprocessing import Pool from multiprocessing import Pool
@@ -38,11 +38,12 @@ from PIL import Image
from subprocess import STDOUT, PIPE from subprocess import STDOUT, PIPE
from psutil import Popen, virtual_memory from psutil import Popen, virtual_memory
from scandir import walk from scandir import walk
from html import escape
try: try:
from PyQt5 import QtCore from PyQt5 import QtCore
except ImportError: except ImportError:
QtCore = None QtCore = None
from .shared import md5Checksum, getImageFileName, walkSort, walkLevel, saferReplace from .shared import md5Checksum, getImageFileName, walkSort, walkLevel, saferReplace
from . import comic2panel from . import comic2panel
from . import image from . import image
from . import cbxarchive from . import cbxarchive
@@ -65,6 +66,7 @@ def main(argv=None):
print('No matching files found.') print('No matching files found.')
return return
for source in sources: for source in sources:
source = source.rstrip('\\').rstrip('/')
options = copy(optionstemplate) options = copy(optionstemplate)
checkOptions() checkOptions()
if len(sources) > 1: if len(sources) > 1:
@@ -241,9 +243,9 @@ def buildNCX(dstdir, title, chapters, chapterNames):
navID = filename[0].replace('/', '_').replace('\\', '_') navID = filename[0].replace('/', '_').replace('\\', '_')
elif os.path.basename(folder) != "Text": elif os.path.basename(folder) != "Text":
title = chapterNames[os.path.basename(folder)] title = chapterNames[os.path.basename(folder)]
f.write("<navPoint id=\"" + navID + "\"><navLabel><text>" f.write("<navPoint id=\"" + navID + "\"><navLabel><text>" +
+ title + "</text></navLabel><content src=\"" + filename[0].replace("\\", "/") title + "</text></navLabel><content src=\"" + filename[0].replace("\\", "/") +
+ ".html\"/></navPoint>\n") ".html\"/></navPoint>\n")
f.write("</navMap>\n</ncx>") f.write("</navMap>\n</ncx>")
f.close() f.close()
@@ -313,18 +315,15 @@ def buildOPF(dstdir, title, filelist, cover=None):
"<meta property=\"rendition:spread\">portrait</meta>\n", "<meta property=\"rendition:spread\">portrait</meta>\n",
"<meta property=\"rendition:layout\">pre-paginated</meta>\n"]) "<meta property=\"rendition:layout\">pre-paginated</meta>\n"])
if options.iskindle and options.profile != 'Custom': if options.iskindle and options.profile != 'Custom':
f.writelines(["<meta property=\"RegionMagnification\">true</meta>\n", f.writelines(["<meta name=\"original-resolution\" content=\"",
"<meta property=\"region-mag\">true</meta>\n", str(deviceres[0]) + "x" + str(deviceres[1]) + "\"/>\n",
"<meta property=\"book-type\">comic</meta>\n", "<meta name=\"book-type\" content=\"comic\"/>\n",
"<meta property=\"zero-gutter\">true</meta>\n", "<meta name=\"RegionMagnification\" content=\"true\"/>\n",
"<meta property=\"zero-margin\">true</meta>\n", "<meta name=\"primary-writing-mode\" content=\"" + writingmode + "\"/>\n",
"<meta property=\"fixed-layout\">true</meta>\n", "<meta name=\"zero-gutter\" content=\"true\"/>\n",
"<meta property=\"orientation-lock\">portrait</meta>\n", "<meta name=\"zero-margin\" content=\"true\"/>\n",
"<meta property=\"original-resolution\">", "<meta name=\"ke-border-color\" content=\"#ffffff\"/>\n",
str(deviceres[0]) + "x" + str(deviceres[1]) + "</meta>\n", "<meta name=\"ke-border-width\" content=\"0\"/>\n"])
"<meta property=\"primary-writing-mode\">" + writingmode + "</meta>\n",
"<meta property=\"ke-border-color\">#ffffff</meta>\n",
"<meta property=\"ke-border-width\">0</meta>\n"])
f.writelines(["</metadata>\n<manifest>\n<item id=\"ncx\" href=\"toc.ncx\" ", f.writelines(["</metadata>\n<manifest>\n<item id=\"ncx\" href=\"toc.ncx\" ",
"media-type=\"application/x-dtbncx+xml\"/>\n", "media-type=\"application/x-dtbncx+xml\"/>\n",
"<item id=\"nav\" href=\"nav.xhtml\" ", "<item id=\"nav\" href=\"nav.xhtml\" ",
@@ -343,15 +342,15 @@ def buildOPF(dstdir, title, filelist, cover=None):
filename = getImageFileName(path[1]) filename = getImageFileName(path[1])
uniqueid = os.path.join(folder, filename[0]).replace('/', '_').replace('\\', '_') uniqueid = os.path.join(folder, filename[0]).replace('/', '_').replace('\\', '_')
reflist.append(uniqueid) reflist.append(uniqueid)
f.write("<item id=\"page_" + str(uniqueid) + "\" href=\"" f.write("<item id=\"page_" + str(uniqueid) + "\" href=\"" +
+ folder.replace('Images', 'Text') + "/" + filename[0] folder.replace('Images', 'Text') + "/" + filename[0] +
+ ".html\" media-type=\"application/xhtml+xml\"/>\n") ".html\" media-type=\"application/xhtml+xml\"/>\n")
if '.png' == filename[1]: if '.png' == filename[1]:
mt = 'image/png' mt = 'image/png'
else: else:
mt = 'image/jpeg' mt = 'image/jpeg'
f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" +
+ mt + "\"/>\n") mt + "\"/>\n")
f.write("<item id=\"css\" href=\"Text/style.css\" media-type=\"text/css\"/>\n") f.write("<item id=\"css\" href=\"Text/style.css\" media-type=\"text/css\"/>\n")
if options.righttoleft: if options.righttoleft:
f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n") f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n")
@@ -649,44 +648,38 @@ def imgFileProcessing(work):
def getWorkFolder(afile): def getWorkFolder(afile):
if len(afile) > 240:
raise UserWarning("Path is too long.")
if os.path.isdir(afile): if os.path.isdir(afile):
workdir = mkdtemp('', 'KCC-') workdir = mkdtemp('', 'KCC-')
try: try:
os.rmdir(workdir) os.rmdir(workdir)
fullPath = os.path.join(workdir, 'OEBPS', 'Images') fullPath = os.path.join(workdir, 'OEBPS', 'Images')
if len(fullPath) > 240:
raise UserWarning("Path is too long.")
copytree(afile, fullPath) copytree(afile, fullPath)
sanitizePermissions(fullPath) sanitizePermissions(fullPath)
return workdir return workdir
except OSError: except:
rmtree(workdir, True) rmtree(workdir, True)
raise raise UserWarning("Failed to prepare a workspace.")
elif afile.lower().endswith('.pdf'): elif afile.lower().endswith('.pdf'):
pdf = pdfjpgextract.PdfJpgExtract(afile) pdf = pdfjpgextract.PdfJpgExtract(afile)
path, njpg = pdf.extract() path, njpg = pdf.extract()
if njpg == 0: if njpg == 0:
rmtree(path, True) rmtree(path, True)
raise UserWarning("Failed to extract images.") raise UserWarning("Failed to extract images from PDF file.")
else: else:
workdir = mkdtemp('', 'KCC-') workdir = mkdtemp('', 'KCC-')
cbx = cbxarchive.CBxArchive(afile) cbx = cbxarchive.CBxArchive(afile)
if cbx.isCbxFile(): if cbx.isCbxFile():
try: try:
path = cbx.extract(workdir) path = cbx.extract(workdir)
except OSError: except:
rmtree(workdir, True) rmtree(workdir, True)
raise UserWarning("Failed to extract file.") raise UserWarning("Failed to extract archive.")
else: else:
rmtree(workdir, True) rmtree(workdir, True)
raise TypeError("Failed to detect archive format.") raise UserWarning("Failed to detect archive format.")
if len(os.path.join(path, 'OEBPS', 'Images')) > 240: newpath = mkdtemp('', 'KCC-')
raise UserWarning("Path is too long.") move(path, os.path.join(newpath, 'OEBPS', 'Images'))
move(path, path + "_temp") return newpath
move(path + "_temp", os.path.join(path, 'OEBPS', 'Images'))
return path
def getOutputFilename(srcpath, wantedname, ext, tomeNumber): def getOutputFilename(srcpath, wantedname, ext, tomeNumber):
@@ -746,7 +739,7 @@ def getComicInfo(path, originalPath):
options.authors = [] options.authors = []
if defaultTitle: if defaultTitle:
if xml.data['Series']: if xml.data['Series']:
options.title = xml.data['Series'] options.title = escape(xml.data['Series'])
if xml.data['Volume']: if xml.data['Volume']:
titleSuffix += ' V' + xml.data['Volume'] titleSuffix += ' V' + xml.data['Volume']
if xml.data['Number']: if xml.data['Number']:
@@ -754,7 +747,7 @@ def getComicInfo(path, originalPath):
options.title += titleSuffix options.title += titleSuffix
for field in ['Writers', 'Pencillers', 'Inkers', 'Colorists']: for field in ['Writers', 'Pencillers', 'Inkers', 'Colorists']:
for person in xml.data[field]: for person in xml.data[field]:
options.authors.append(person) options.authors.append(escape(person))
if len(options.authors) > 0: if len(options.authors) > 0:
options.authors = list(set(options.authors)) options.authors = list(set(options.authors))
options.authors.sort() options.authors.sort()
@@ -765,7 +758,7 @@ def getComicInfo(path, originalPath):
if xml.data['Bookmarks']: if xml.data['Bookmarks']:
options.chapters = xml.data['Bookmarks'] options.chapters = xml.data['Bookmarks']
if xml.data['Summary']: if xml.data['Summary']:
options.summary = xml.data['Summary'] options.summary = escape(xml.data['Summary'])
os.remove(xmlPath) os.remove(xmlPath)
@@ -988,7 +981,7 @@ def detectCorruption(tmpPath, orgPath):
imageSmaller += 1 imageSmaller += 1
except Exception as err: except Exception as err:
rmtree(os.path.join(tmpPath, '..', '..'), True) rmtree(os.path.join(tmpPath, '..', '..'), True)
if 'decoder' in err and 'not available' in err: if 'decoder' in str(err) and 'not available' in str(err):
raise RuntimeError('Pillow was compiled without JPG and/or PNG decoder.') raise RuntimeError('Pillow was compiled without JPG and/or PNG decoder.')
else: else:
raise RuntimeError('Image file %s is corrupted.' % pathOrg) raise RuntimeError('Image file %s is corrupted.' % pathOrg)
@@ -1041,7 +1034,7 @@ def createNewTome():
def slugify(value): def slugify(value):
value = slugifyExt(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 return value
@@ -1183,7 +1176,7 @@ def checkOptions():
if options.customheight != 0: if options.customheight != 0:
Y = options.customheight Y = options.customheight
newProfile = ("Custom", (int(X), int(Y)), image.ProfileData.Palette16, 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 image.ProfileData.Profiles["Custom"] = newProfile
options.profile = "Custom" options.profile = "Custom"
options.profileData = image.ProfileData.Profiles[options.profile] options.profileData = image.ProfileData.Profiles[options.profile]
@@ -1210,6 +1203,21 @@ def checkTools(source):
exit(1) 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): def makeBook(source, qtGUI=None):
"""Generates MOBI/EPUB/CBZ comic ebook from a bunch of images.""" """Generates MOBI/EPUB/CBZ comic ebook from a bunch of images."""
global GUI global GUI
@@ -1218,6 +1226,7 @@ def makeBook(source, qtGUI=None):
GUI.progressBarTick.emit('1') GUI.progressBarTick.emit('1')
else: else:
checkTools(source) checkTools(source)
checkPre(source)
path = getWorkFolder(source) path = getWorkFolder(source)
print("\nChecking images...") print("\nChecking images...")
getComicInfo(os.path.join(path, "OEBPS", "Images"), source) getComicInfo(os.path.join(path, "OEBPS", "Images"), source)
@@ -1350,7 +1359,7 @@ def makeMOBI(work, qtGUI=None):
global GUI, makeMOBIWorkerPool, makeMOBIWorkerOutput global GUI, makeMOBIWorkerPool, makeMOBIWorkerOutput
GUI = qtGUI GUI = qtGUI
makeMOBIWorkerOutput = [] makeMOBIWorkerOutput = []
availableMemory = virtual_memory().total/1000000000 availableMemory = virtual_memory().total / 1000000000
if availableMemory <= 2: if availableMemory <= 2:
threadNumber = 1 threadNumber = 1
elif 2 < availableMemory <= 4: elif 2 < availableMemory <= 4:

View File

@@ -85,19 +85,19 @@ def sanitizePanelSize(panel, opt):
newPanels = [] newPanels = []
if panel[2] > 6 * opt.height: if panel[2] > 6 * opt.height:
diff = int(panel[2] / 8) diff = int(panel[2] / 8)
newPanels.append([panel[0], panel[1] - diff*7, 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 * 7, panel[1] - diff * 6, diff])
newPanels.append([panel[1] - diff*6, panel[1] - diff*5, 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 * 5, panel[1] - diff * 4, diff])
newPanels.append([panel[1] - diff*4, panel[1] - diff*3, 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 * 3, panel[1] - diff * 2, diff])
newPanels.append([panel[1] - diff*2, panel[1] - diff, diff]) newPanels.append([panel[1] - diff * 2, panel[1] - diff, diff])
newPanels.append([panel[1] - diff, panel[1], diff]) newPanels.append([panel[1] - diff, panel[1], diff])
elif panel[2] > 3 * opt.height: elif panel[2] > 3 * opt.height:
diff = int(panel[2] / 4) diff = int(panel[2] / 4)
newPanels.append([panel[0], panel[1] - diff*3, 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 * 3, panel[1] - diff * 2, diff])
newPanels.append([panel[1] - diff*2, panel[1] - diff, diff]) newPanels.append([panel[1] - diff * 2, panel[1] - diff, diff])
newPanels.append([panel[1] - diff, panel[1], diff]) newPanels.append([panel[1] - diff, panel[1], diff])
elif panel[2] > 1.5 * opt.height: elif panel[2] > 1.5 * opt.height:
newPanels.append([panel[0], panel[1] - int(panel[2] / 2), int(panel[2] / 2)]) newPanels.append([panel[0], panel[1] - int(panel[2] / 2), int(panel[2] / 2)])

View File

@@ -36,15 +36,15 @@ title_offset = 84
def getint(data, ofs, sz='L'): def getint(data, ofs, sz='L'):
i, = struct.unpack_from('>'+sz, data, ofs) i, = struct.unpack_from('>' + sz, data, ofs)
return i return i
def writeint(data, ofs, n, slen='L'): def writeint(data, ofs, n, slen='L'):
if 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: 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): def getsecaddr(datain, secno):
@@ -52,11 +52,11 @@ def getsecaddr(datain, secno):
if (secno < 0) | (secno >= nsec): if (secno < 0) | (secno >= nsec):
emsg = 'requested section number %d out of range (nsec=%d)' % (secno, nsec) emsg = 'requested section number %d out of range (nsec=%d)' % (secno, nsec)
raise DualMetaFixException(emsg) raise DualMetaFixException(emsg)
secstart = getint(datain, first_pdb_record+secno*8) secstart = getint(datain, first_pdb_record + secno * 8)
if secno == nsec-1: if secno == nsec - 1:
secend = len(datain) secend = len(datain)
else: else:
secend = getint(datain, first_pdb_record+(secno+1)*8) secend = getint(datain, first_pdb_record + (secno + 1) * 8)
return secstart, secend return secstart, secend
@@ -71,28 +71,28 @@ def replacesection(datain, secno, secdata):
seclen = secend - secstart seclen = secend - secstart
if len(secdata) != seclen: if len(secdata) != seclen:
raise DualMetaFixException('section length change in replacesection') raise DualMetaFixException('section length change in replacesection')
datain[secstart:secstart+seclen] = secdata datain[secstart:secstart + seclen] = secdata
def get_exth_params(rec0): def get_exth_params(rec0):
ebase = mobi_header_base + getint(rec0, mobi_header_length) 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') raise DualMetaFixException('EXTH tag not found where expected')
elen = getint(rec0, ebase+4) elen = getint(rec0, ebase + 4)
enum = getint(rec0, ebase+8) enum = getint(rec0, ebase + 8)
rlen = len(rec0) rlen = len(rec0)
return ebase, elen, enum, rlen return ebase, elen, enum, rlen
def add_exth(rec0, exth_num, exth_bytes): def add_exth(rec0, exth_num, exth_bytes):
ebase, elen, enum, rlen = get_exth_params(rec0) ebase, elen, enum, rlen = get_exth_params(rec0)
newrecsize = 8+len(exth_bytes) 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)\ newrec0 = rec0[0:ebase + 4] + struct.pack('>L', elen + newrecsize) + struct.pack('>L', enum + 1) + \
+ struct.pack('>L', newrecsize)+exth_bytes+rec0[ebase+12:] struct.pack('>L', exth_num) + struct.pack('>L', newrecsize) + exth_bytes + rec0[ebase + 12:]
newrec0 = writeint(newrec0, title_offset, getint(newrec0, title_offset)+newrecsize) newrec0 = writeint(newrec0, title_offset, getint(newrec0, title_offset) + newrecsize)
# keep constant record length by removing newrecsize null bytes from end # keep constant record length by removing newrecsize null bytes from end
sectail = newrec0[-newrecsize:] 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') raise DualMetaFixException('add_exth: trimmed non-null bytes at end of section')
newrec0 = newrec0[0:rlen] newrec0 = newrec0[0:rlen]
return newrec0 return newrec0
@@ -106,30 +106,31 @@ def read_exth(rec0, exth_num):
exth_id = getint(rec0, ebase) exth_id = getint(rec0, ebase)
if exth_id == exth_num: if exth_id == exth_num:
# We might have multiple exths, so build a list. # 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 enum -= 1
ebase = ebase+getint(rec0, ebase+4) ebase = ebase + getint(rec0, ebase + 4)
return exth_values return exth_values
def del_exth(rec0, exth_num): def del_exth(rec0, exth_num):
ebase, elen, enum, rlen = get_exth_params(rec0) ebase, elen, enum, rlen = get_exth_params(rec0)
ebase_idx = ebase+12 ebase_idx = ebase + 12
enum_idx = 0 enum_idx = 0
while enum_idx < enum: while enum_idx < enum:
exth_id = getint(rec0, ebase_idx) 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: if exth_id == exth_num:
newrec0 = rec0 newrec0 = rec0
newrec0 = writeint(newrec0, title_offset, getint(newrec0, title_offset)-exth_size) newrec0 = writeint(newrec0, title_offset, getint(newrec0, title_offset) - exth_size)
newrec0 = newrec0[:ebase_idx]+newrec0[ebase_idx+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 = newrec0[0:ebase + 4] + struct.pack('>L', elen - exth_size) + \
newrec0 += b'\0'*exth_size struct.pack('>L', enum - 1) + newrec0[ebase + 12:]
newrec0 += b'\0' * exth_size
if rlen != len(newrec0): if rlen != len(newrec0):
raise DualMetaFixException('del_exth: incorrect section size change') raise DualMetaFixException('del_exth: incorrect section size change')
return newrec0 return newrec0
enum_idx += 1 enum_idx += 1
ebase_idx = ebase_idx+exth_size ebase_idx = ebase_idx + exth_size
return rec0 return rec0

View File

@@ -148,8 +148,8 @@ class ComicPage:
if self.noVPV: if self.noVPV:
flags.append('NoVerticalPanelView') flags.append('NoVerticalPanelView')
if self.border: if self.border:
flags.append('Margins-' + str(self.border[0]) + '-' + str(self.border[1]) + '-' flags.append('Margins-' + str(self.border[0]) + '-' + str(self.border[1]) + '-' +
+ str(self.border[2]) + '-' + str(self.border[3])) str(self.border[2]) + '-' + str(self.border[3]))
if self.fill != 'white': if self.fill != 'white':
flags.append('BlackFill') flags.append('BlackFill')
if self.opt.quality == 2: if self.opt.quality == 2:
@@ -199,10 +199,10 @@ class ComicPage:
else: else:
multiplier = 1.5 multiplier = 1.5
if border is not None: if border is not None:
self.border = [round(float(border[0])/float(self.image.size[0])*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(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[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)] 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: if int((border[2] - border[0]) * multiplier) < self.size[0] + 10:
self.noHPV = True self.noHPV = True
if int((border[3] - border[1]) * multiplier) < self.size[1] + 10: if int((border[3] - border[1]) * multiplier) < self.size[1] + 10:
@@ -428,13 +428,13 @@ class ComicPage:
while startY < bw.size[1]: while startY < bw.size[1]:
if startY + 5 > bw.size[1]: if startY + 5 > bw.size[1]:
startY = bw.size[1] - 5 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 startY += 5
startX = 0 startX = 0
while startX < bw.size[0]: while startX < bw.size[0]:
if startX + 5 > bw.size[0]: if startX + 5 > bw.size[0]:
startX = bw.size[0] - 5 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 startX += 5
if fill > 0: if fill > 0:
self.fill = 'black' self.fill = 'black'
@@ -442,29 +442,25 @@ class ComicPage:
self.fill = 'white' self.fill = 'white'
def isImageColor(self): def isImageColor(self):
v = ImageStat.Stat(self.image).var img = self.image.copy()
isMonochromatic = reduce(lambda x, y: x and y < 0.005, v, True) bands = img.getbands()
if isMonochromatic: if bands == ('R', 'G', 'B') or bands == ('R', 'G', 'B', 'A'):
# Monochromatic thumb = img.resize((40, 40))
return False SSE, bias = 0, [0, 0, 0]
else: bias = ImageStat.Stat(thumb).mean[:3]
if len(v) == 3: bias = [b - sum(bias) / 3 for b in bias]
maxmin = abs(max(v) - min(v)) for pixel in thumb.getdata():
if maxmin > 1000: mu = sum(pixel) / 3
# Color SSE += sum((pixel[i] - mu - bias[i]) * (pixel[i] - mu - bias[i]) for i in [0, 1, 2])
return True MSE = float(SSE) / (40 * 40)
elif maxmin > 100: if MSE <= 22:
# Probably color
return True
else:
# Grayscale
return False
elif len(v) == 1:
# Black and white
return False return False
else: else:
# Detection failed return True
return False elif len(bands) == 1:
return False
else:
return False
class Cover: class Cover:
@@ -513,4 +509,4 @@ class Cover:
try: try:
self.image.save(self.target, "JPEG", optimize=1, quality=80) self.image.save(self.target, "JPEG", optimize=1, quality=80)
except IOError: except IOError:
raise RuntimeError('Failed to save cover') raise RuntimeError('Failed to process downloaded cover.')

View File

@@ -360,9 +360,8 @@ class RarCannotExec(RarExecError):
def is_rarfile(xfile): def is_rarfile(xfile):
'''Check quickly whether file is rar archive.''' '''Check quickly whether file is rar archive.'''
fd = XFile(xfile) with open(xfile, 'rb') as fh:
buf = fd.read(len(RAR_ID)) buf = fh.read(len(RAR_ID))
fd.close()
if buf == RAR_ID or buf == RAR5_ID: if buf == RAR_ID or buf == RAR5_ID:
return True return True
else: else:

View File

@@ -25,6 +25,7 @@ from shutil import rmtree, move
from tempfile import mkdtemp from tempfile import mkdtemp
from zipfile import ZipFile, ZIP_DEFLATED from zipfile import ZipFile, ZIP_DEFLATED
from re import split from re import split
from traceback import format_tb
try: try:
from scandir import walk from scandir import walk
except ImportError: except ImportError:
@@ -45,6 +46,9 @@ class HTMLStripper(HTMLParser):
def get_data(self): def get_data(self):
return ''.join(self.fed) return ''.join(self.fed)
def error(self, message):
pass
def getImageFileName(imgfile): def getImageFileName(imgfile):
name, ext = os.path.splitext(imgfile) name, ext = os.path.splitext(imgfile)
@@ -117,16 +121,22 @@ def removeFromZIP(zipfname, *filenames):
rmtree(tempdir) 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): def dependencyCheck(level):
missing = [] missing = []
if level > 2: if level > 2:
try: try:
from PyQt5.QtCore import qVersion as qtVersion from PyQt5.QtCore import qVersion as qtVersion
if StrictVersion('5.2.0') > StrictVersion(qtVersion()): if StrictVersion('5.4.0') > StrictVersion(qtVersion()):
missing.append('PyQt 5.2.0+') missing.append('PyQt 5.4.0+')
except ImportError: except ImportError:
missing.append('PyQt 5.2.0+') missing.append('PyQt 5.4.0+')
if level > 1: if level > 1:
try: try:
from psutil import __version__ as psutilVersion from psutil import __version__ as psutilVersion
@@ -136,10 +146,10 @@ def dependencyCheck(level):
missing.append('psutil 3.0.0+') missing.append('psutil 3.0.0+')
try: try:
from slugify import __version__ as slugifyVersion from slugify import __version__ as slugifyVersion
if StrictVersion('1.1.2') > StrictVersion(slugifyVersion): if StrictVersion('1.1.3') > StrictVersion(slugifyVersion):
missing.append('python-slugify 1.1.2+') missing.append('python-slugify 1.1.3+')
except ImportError: except ImportError:
missing.append('python-slugify 1.1.2+') missing.append('python-slugify 1.1.3+')
try: try:
from PIL import PILLOW_VERSION as pillowVersion from PIL import PILLOW_VERSION as pillowVersion
if StrictVersion('2.8.2') > StrictVersion(pillowVersion): if StrictVersion('2.8.2') > StrictVersion(pillowVersion):

10
setup.json Normal file
View File

@@ -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" }
]
}

View File

@@ -22,10 +22,9 @@ VERSION = __version__
MAIN = 'kcc.py' MAIN = 'kcc.py'
extra_options = {} extra_options = {}
# noinspection PyUnresolvedReferences
if platform == 'darwin': if platform == 'darwin':
from setuptools import setup from setuptools import setup
from os import chmod, makedirs from os import chmod, makedirs, system
from shutil import copyfile from shutil import copyfile
extra_options = dict( extra_options = dict(
setup_requires=['py2app'], setup_requires=['py2app'],
@@ -34,10 +33,10 @@ if platform == 'darwin':
py2app=dict( py2app=dict(
argv_emulation=True, argv_emulation=True,
iconfile='icons/comic2ebook.icns', iconfile='icons/comic2ebook.icns',
includes=['sip', 'PyQt5.QtPrintSupport'], includes=['sip'],
resources=['LICENSE.txt', 'other/qt.conf', 'other/Additional-LICENSE.txt', 'other/unrar', 'other/7za'], resources=['LICENSE.txt', 'other/qt.conf', 'other/Additional-LICENSE.txt', 'other/unrar', 'other/7za'],
plist=dict( plist=dict(
CFBundleName=NAME, CFBundleName='Kindle Comic Converter',
CFBundleShortVersionString=VERSION, CFBundleShortVersionString=VERSION,
CFBundleGetInfoString=NAME + ' ' + VERSION + CFBundleGetInfoString=NAME + ' ' + VERSION +
', written 2012-2015 by Ciro Mattia Gonano and Pawel Jastrzebski', ', written 2012-2015 by Ciro Mattia Gonano and Pawel Jastrzebski',
@@ -60,7 +59,6 @@ if platform == 'darwin':
) )
) )
elif platform == 'win32': elif platform == 'win32':
# noinspection PyUnresolvedReferences
import py2exe import py2exe
from platform import architecture from platform import architecture
from distutils.core import setup from distutils.core import setup
@@ -95,7 +93,7 @@ elif platform == 'win32':
zipfile=None, zipfile=None,
data_files=additional_files) data_files=additional_files)
else: else:
if argv[1] == 'make_pyz': if len(argv) > 1 and argv[1] == 'make_pyz':
from os import system from os import system
script = ''' script = '''
cp kcc.py __main__.py cp kcc.py __main__.py
@@ -137,7 +135,7 @@ else:
install_requires=[ install_requires=[
'Pillow>=2.8.2', 'Pillow>=2.8.2',
'psutil>=3.0.0', 'psutil>=3.0.0',
'python-slugify>=1.1.2', 'python-slugify>=1.1.3',
'scandir>=1.1.0', 'scandir>=1.1.0',
], ],
zip_safe=False, zip_safe=False,
@@ -156,7 +154,8 @@ setup(
) )
if platform == 'darwin': if platform == 'darwin':
makedirs('dist/' + NAME + '.app/Contents/PlugIns/platforms', exist_ok=True) makedirs('dist/Kindle Comic Converter.app/Contents/PlugIns/platforms', exist_ok=True)
copyfile('other/libqcocoa.dylib', 'dist/' + NAME + '.app/Contents/PlugIns/platforms/libqcocoa.dylib') copyfile('other/libqcocoa.dylib', 'dist/Kindle Comic Converter.app/Contents/PlugIns/platforms/libqcocoa.dylib')
chmod('dist/' + NAME + '.app/Contents/Resources/unrar', 0o777) chmod('dist/Kindle Comic Converter.app/Contents/Resources/unrar', 0o777)
chmod('dist/' + NAME + '.app/Contents/Resources/7za', 0o777) chmod('dist/Kindle Comic Converter.app/Contents/Resources/7za', 0o777)
system('appdmg setup.json dist/KindleComicConverter_osx_' + VERSION + '.dmg')