1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-15 13:38:46 +00:00

Compare commits

...

42 Commits
4.6 ... 4.6.5

Author SHA1 Message Date
Paweł Jastrzębski
1fa6d315b1 Merge pull request #156 from ciromattia/dev
4.6.5
2015-09-17 15:44:42 +02:00
Paweł Jastrzębski
112917754a Fixed OS X GUI anomalies 2015-09-17 10:35:54 +02:00
Paweł Jastrzębski
65774c6f12 Updated README + version bump 2015-09-16 19:13:32 +02:00
Paweł Jastrzębski
7b3ce8827f Updated OS X build environment 2015-09-16 19:00:45 +02:00
Paweł Jastrzębski
b12825045b Fixed stupid typo 2015-09-16 11:52:17 +02:00
Paweł Jastrzębski
ac9f3a5d87 KindleGen detection tweak 2015-09-16 10:22:53 +02:00
Paweł Jastrzębski
00969a3739 Allow older PyQT 2015-09-16 08:47:59 +02:00
Paweł Jastrzębski
21f738b44a Yet another Windows file lock fix 2015-09-15 21:24:49 +02:00
Paweł Jastrzębski
f2238b16a6 os.access acts unpredictably on Windows 2015-09-15 17:44:55 +02:00
Paweł Jastrzębski
14f677ec68 Python 3.5+ include scandir 2015-09-15 15:44:52 +02:00
Paweł Jastrzębski
7b3bf4618f Yet another Windows file lock fixes 2015-09-12 09:48:17 +02:00
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
Paweł Jastrzębski
a268e12a90 Merge pull request #147 from ciromattia/dev
4.6.1
2015-07-05 12:16:27 +02:00
Paweł Jastrzębski
d621335e6c Updated README + version bump 2015-07-05 12:15:35 +02:00
Paweł Jastrzębski
ec1d9c2d93 Added ComicRack Summary field parsing (close #146) 2015-07-05 07:49:48 +02:00
Paweł Jastrzębski
85b9dbbf83 Detect too small input images 2015-07-05 06:57:56 +02:00
Paweł Jastrzębski
feeced44bf Tweaked KEPUB renamer (close #144) 2015-06-28 19:20:38 +02:00
Paweł Jastrzębski
cbea18398b Fixed Kobo TOC (close #145) 2015-06-28 18:31:47 +02:00
22 changed files with 365 additions and 206 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.2.1+
- [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,30 @@ 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.5:
* Fixed multiple Windows and OS X issues
* Allowed Linux release to use older PyQT5 version
####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:
* Fixed KEPUB TOC generator
* Added warning about too small input files
* ComicRack Summary metadata field is now parsed
* Small tweaks of KEPUB output
####4.6: ####4.6:
* KEPUB is now default output for all Kobo profiles * KEPUB is now default output for all Kobo profiles
* EPUB output now produce fully valid EPUB 3.0.1 * EPUB output now produce fully valid EPUB 3.0.1
@@ -413,6 +437,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" #define MyAppVersion "4.6.5"
#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"

1
kcc.py
View File

@@ -30,6 +30,7 @@ if sys.platform.startswith('darwin'):
os.environ['PATH'] = os.path.dirname(os.path.abspath(__file__)) + '/other/:' + os.environ['PATH'] os.environ['PATH'] = os.path.dirname(os.path.abspath(__file__)) + '/other/:' + os.environ['PATH']
else: else:
os.environ['PATH'] = './../Resources:/usr/local/bin:/usr/bin:/bin' os.environ['PATH'] = './../Resources:/usr/local/bin:/usr/bin:/bin'
os.system('defaults write com.kindlecomicconverter.KindleComicConverter ApplePersistenceIgnoreState YES')
elif sys.platform.startswith('win'): elif sys.platform.startswith('win'):
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
os.chdir(os.path.dirname(os.path.abspath(sys.executable))) os.chdir(os.path.dirname(os.path.abspath(sys.executable)))

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()
@@ -1103,6 +1127,8 @@ class KCCGUI(KCC_ui.Ui_KCC):
'<b>KindleGen</b></a>! MOBI conversion will be unavailable!', 'error') '<b>KindleGen</b></a>! MOBI conversion will be unavailable!', 'error')
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
self.addMessage('Download it and place EXE in KCC directory.', 'error') self.addMessage('Download it and place EXE in KCC directory.', 'error')
elif sys.platform.startswith('darwin'):
self.addMessage('Install it using <a href="http://brew.sh/">Brew</a>.', 'error')
else: else:
self.addMessage('Download it and place executable in /usr/local/bin directory.', 'error') self.addMessage('Download it and place executable in /usr/local/bin directory.', 'error')
@@ -1205,7 +1231,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 +1299,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 +1333,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 +1380,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' __version__ = '4.6.5'
__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

@@ -22,7 +22,10 @@ from zipfile import is_zipfile, ZipFile
from subprocess import STDOUT, PIPE from subprocess import STDOUT, PIPE
from psutil import Popen from psutil import Popen
from shutil import move, copy from shutil import move, copy
from scandir import walk try:
from scandir import walk
except ImportError:
walk = os.walk
from . import rarfile from . import rarfile
from .shared import check7ZFile as is_7zfile, saferReplace from .shared import check7ZFile as is_7zfile, saferReplace
@@ -70,8 +73,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, TemporaryFile
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
@@ -37,12 +37,16 @@ from slugify import slugify as slugifyExt
from PIL import Image 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 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 try:
from scandir import walk
except ImportError:
walk = os.walk
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 +69,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 +246,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()
@@ -260,7 +265,16 @@ def buildNAV(dstdir, title, chapters, chapterNames):
"</head>\n", "</head>\n",
"<body>\n", "<body>\n",
"<nav xmlns:epub=\"http://www.idpf.org/2007/ops\" epub:type=\"toc\" id=\"toc\">\n", "<nav xmlns:epub=\"http://www.idpf.org/2007/ops\" epub:type=\"toc\" id=\"toc\">\n",
"<ol></ol>\n", "<ol>\n"])
for chapter in chapters:
folder = chapter[0].replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\')
filename = getImageFileName(os.path.join(folder, chapter[1]))
if options.chapters:
title = chapterNames[chapter[1]]
elif os.path.basename(folder) != "Text":
title = chapterNames[os.path.basename(folder)]
f.write("<li><a href=\"" + filename[0].replace("\\", "/") + ".html\">" + title + "</a></li>\n")
f.writelines(["</ol>\n",
"</nav>\n", "</nav>\n",
"<nav epub:type=\"page-list\">\n", "<nav epub:type=\"page-list\">\n",
"<ol>\n" "<ol>\n"
@@ -294,7 +308,8 @@ def buildOPF(dstdir, title, filelist, cover=None):
"<dc:title>", title, "</dc:title>\n", "<dc:title>", title, "</dc:title>\n",
"<dc:language>en-US</dc:language>\n", "<dc:language>en-US</dc:language>\n",
"<dc:identifier id=\"BookID\">urn:uuid:", options.uuid, "</dc:identifier>\n", "<dc:identifier id=\"BookID\">urn:uuid:", options.uuid, "</dc:identifier>\n",
"<dc:contributor id=\"contributor\">KindleComicConverter-" + __version__ + "</dc:contributor>\n"]) "<dc:contributor id=\"contributor\">KindleComicConverter-" + __version__ + "</dc:contributor>\n",
"<dc:description>", options.summary, "</dc:description>\n"])
for author in options.authors: for author in options.authors:
f.writelines(["<dc:creator>", author, "</dc:creator>\n"]) f.writelines(["<dc:creator>", author, "</dc:creator>\n"])
f.writelines(["<meta property=\"dcterms:modified\">" + strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + "</meta>\n", f.writelines(["<meta property=\"dcterms:modified\">" + strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + "</meta>\n",
@@ -303,18 +318,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\" ",
@@ -333,15 +345,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")
@@ -639,51 +651,46 @@ 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.") copytree(path, os.path.join(newpath, 'OEBPS', 'Images'))
move(path, path + "_temp") rmtree(path, True)
move(path + "_temp", os.path.join(path, 'OEBPS', 'Images')) return newpath
return path
def getOutputFilename(srcpath, wantedname, ext, tomeNumber): def getOutputFilename(srcpath, wantedname, ext, tomeNumber):
if srcpath[-1] == os.path.sep: if srcpath[-1] == os.path.sep:
srcpath = srcpath[:-1] srcpath = srcpath[:-1]
if not ext.startswith('.'): if 'Ko' in options.profile and options.format == 'EPUB':
ext = '.' + ext ext = '.kepub.epub'
if wantedname is not None: if wantedname is not None:
if wantedname.endswith(ext): if wantedname.endswith(ext):
filename = os.path.abspath(wantedname) filename = os.path.abspath(wantedname)
@@ -695,7 +702,14 @@ def getOutputFilename(srcpath, wantedname, ext, tomeNumber):
elif os.path.isdir(srcpath): elif os.path.isdir(srcpath):
filename = srcpath + tomeNumber + ext filename = srcpath + tomeNumber + ext
else: else:
filename = os.path.splitext(srcpath)[0] + tomeNumber + ext if 'Ko' in options.profile and options.format == 'EPUB':
path = srcpath.split(os.path.sep)
path[-1] = ''.join(e for e in path[-1].split('.')[0] if e.isalnum()) + tomeNumber + ext
if not path[-1].split('.')[0]:
path[-1] = 'KCCPlaceholder' + tomeNumber + ext
filename = os.path.sep.join(path)
else:
filename = os.path.splitext(srcpath)[0] + tomeNumber + ext
if os.path.isfile(filename): if os.path.isfile(filename):
counter = 0 counter = 0
basename = os.path.splitext(filename)[0] basename = os.path.splitext(filename)[0]
@@ -710,6 +724,7 @@ def getComicInfo(path, originalPath):
options.authors = ['KCC'] options.authors = ['KCC']
options.remoteCovers = {} options.remoteCovers = {}
options.chapters = [] options.chapters = []
options.summary = ''
titleSuffix = '' titleSuffix = ''
if options.title == 'defaulttitle': if options.title == 'defaulttitle':
defaultTitle = True defaultTitle = True
@@ -728,7 +743,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']:
@@ -736,7 +751,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()
@@ -746,6 +761,8 @@ def getComicInfo(path, originalPath):
options.remoteCovers = getCoversFromMCB(xml.data['MUid']) options.remoteCovers = getCoversFromMCB(xml.data['MUid'])
if xml.data['Bookmarks']: if xml.data['Bookmarks']:
options.chapters = xml.data['Bookmarks'] options.chapters = xml.data['Bookmarks']
if xml.data['Summary']:
options.summary = escape(xml.data['Summary'])
os.remove(xmlPath) os.remove(xmlPath)
@@ -948,6 +965,8 @@ def splitProcess(path, mode):
def detectCorruption(tmpPath, orgPath): def detectCorruption(tmpPath, orgPath):
imageNumber = 0
imageSmaller = 0
for root, dirs, files in walk(tmpPath, False): for root, dirs, files in walk(tmpPath, False):
for name in files: for name in files:
if getImageFileName(name) is not None: if getImageFileName(name) is not None:
@@ -961,14 +980,24 @@ def detectCorruption(tmpPath, orgPath):
img.verify() img.verify()
img = Image.open(path) img = Image.open(path)
img.load() img.load()
imageNumber += 1
if options.profileData[1][0] > img.size[0] and options.profileData[1][1] > img.size[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)
else: else:
os.remove(os.path.join(root, name)) os.remove(os.path.join(root, name))
if imageSmaller > imageNumber * 0.5 and not options.upscale and not options.stretch:
print("\nMore than half of images are smaller than target device resolution. "
"Consider enabling stretching or upscaling to improve readability.")
if GUI:
GUI.addMessage.emit('More than half of images are smaller than target device resolution.', 'warning', False)
GUI.addMessage.emit('Consider enabling stretching or upscaling to improve readability.', 'warning', False)
GUI.addMessage.emit('', '', False)
def detectMargins(path): def detectMargins(path):
@@ -1009,7 +1038,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
@@ -1151,7 +1180,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]
@@ -1178,6 +1207,24 @@ 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):
src = os.path.abspath(os.path.join(source, '..'))
else:
src = os.path.dirname(source)
try:
with TemporaryFile(prefix='KCC-', dir=src):
pass
except:
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
@@ -1186,6 +1233,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)
@@ -1239,11 +1287,6 @@ def makeBook(source, qtGUI=None):
filepath.append(getOutputFilename(source, options.output, '.epub', '')) filepath.append(getOutputFilename(source, options.output, '.epub', ''))
makeZIP(tome + '_comic', tome, True) makeZIP(tome + '_comic', tome, True)
move(tome + '_comic.zip', filepath[-1]) move(tome + '_comic.zip', filepath[-1])
if 'Ko' in options.profile:
filename = filepath[-1].split(os.path.sep)
filename[-1] = ''.join(e for e in filename[-1].split('.')[0] if e.isalnum()) + '.kepub.epub'
filename = os.path.sep.join(filename)
move(filepath[-1], filename)
rmtree(tome, True) rmtree(tome, True)
if GUI: if GUI:
GUI.progressBarTick.emit('tick') GUI.progressBarTick.emit('tick')
@@ -1323,7 +1366,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

@@ -24,12 +24,15 @@ from shutil import rmtree, copytree, move
from optparse import OptionParser, OptionGroup from optparse import OptionParser, OptionGroup
from multiprocessing import Pool from multiprocessing import Pool
from PIL import Image, ImageStat, ImageOps from PIL import Image, ImageStat, ImageOps
from scandir import walk
from .shared import getImageFileName, walkLevel, walkSort from .shared import getImageFileName, walkLevel, walkSort
try: try:
from PyQt5 import QtCore from PyQt5 import QtCore
except ImportError: except ImportError:
QtCore = None QtCore = None
try:
from scandir import walk
except ImportError:
walk = os.walk
def mergeDirectoryTick(output): def mergeDirectoryTick(output):
@@ -85,19 +88,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

@@ -38,6 +38,7 @@ class MetadataParser:
'Pencillers': [], 'Pencillers': [],
'Inkers': [], 'Inkers': [],
'Colorists': [], 'Colorists': [],
'Summary': '',
'MUid': '', 'MUid': '',
'Bookmarks': []} 'Bookmarks': []}
self.rawdata = None self.rawdata = None
@@ -74,12 +75,12 @@ class MetadataParser:
extracted = True extracted = True
if not extracted: if not extracted:
rmtree(workdir) rmtree(workdir)
raise OSError raise OSError('Failed to extract 7ZIP file.')
if os.path.isfile(tmpXML): if os.path.isfile(tmpXML):
self.rawdata = parse(tmpXML) self.rawdata = parse(tmpXML)
rmtree(workdir) rmtree(workdir)
else: else:
raise OSError raise OSError('Failed to detect archive format.')
if self.rawdata: if self.rawdata:
self.parseXML() self.parseXML()
@@ -90,6 +91,8 @@ class MetadataParser:
self.data['Volume'] = self.rawdata.getElementsByTagName('Volume')[0].firstChild.nodeValue self.data['Volume'] = self.rawdata.getElementsByTagName('Volume')[0].firstChild.nodeValue
if len(self.rawdata.getElementsByTagName('Number')) != 0: if len(self.rawdata.getElementsByTagName('Number')) != 0:
self.data['Number'] = self.rawdata.getElementsByTagName('Number')[0].firstChild.nodeValue self.data['Number'] = self.rawdata.getElementsByTagName('Number')[0].firstChild.nodeValue
if len(self.rawdata.getElementsByTagName('Summary')) != 0:
self.data['Summary'] = self.rawdata.getElementsByTagName('Summary')[0].firstChild.nodeValue
for field in ['Writer', 'Penciller', 'Inker', 'Colorist']: for field in ['Writer', 'Penciller', 'Inker', 'Colorist']:
if len(self.rawdata.getElementsByTagName(field)) != 0: if len(self.rawdata.getElementsByTagName(field)) != 0:
for person in self.rawdata.getElementsByTagName(field)[0].firstChild.nodeValue.split(', '): for person in self.rawdata.getElementsByTagName(field)[0].firstChild.nodeValue.split(', '):
@@ -113,7 +116,7 @@ class MetadataParser:
for row in (['Series', self.data['Series']], ['Volume', self.data['Volume']], for row in (['Series', self.data['Series']], ['Volume', self.data['Volume']],
['Number', self.data['Number']], ['Writer', ', '.join(self.data['Writers'])], ['Number', self.data['Number']], ['Writer', ', '.join(self.data['Writers'])],
['Penciller', ', '.join(self.data['Pencillers'])], ['Inker', ', '.join(self.data['Inkers'])], ['Penciller', ', '.join(self.data['Pencillers'])], ['Inker', ', '.join(self.data['Inkers'])],
['Colorist', ', '.join(self.data['Colorists'])], ['Colorist', ', '.join(self.data['Colorists'])], ['Summary', self.data['Summary']],
['ScanInformation', 'MCD(' + self.data['MUid'] + ')' if self.data['MUid'] else '']): ['ScanInformation', 'MCD(' + self.data['MUid'] + ')' if self.data['MUid'] else '']):
if self.rawdata.getElementsByTagName(row[0]): if self.rawdata.getElementsByTagName(row[0]):
node = self.rawdata.getElementsByTagName(row[0])[0] node = self.rawdata.getElementsByTagName(row[0])[0]
@@ -135,7 +138,7 @@ class MetadataParser:
for row in (['Series', self.data['Series']], ['Volume', self.data['Volume']], for row in (['Series', self.data['Series']], ['Volume', self.data['Volume']],
['Number', self.data['Number']], ['Writer', ', '.join(self.data['Writers'])], ['Number', self.data['Number']], ['Writer', ', '.join(self.data['Writers'])],
['Penciller', ', '.join(self.data['Pencillers'])], ['Inker', ', '.join(self.data['Inkers'])], ['Penciller', ', '.join(self.data['Pencillers'])], ['Inker', ', '.join(self.data['Inkers'])],
['Colorist', ', '.join(self.data['Colorists'])], ['Colorist', ', '.join(self.data['Colorists'])], ['Summary', self.data['Summary']],
['ScanInformation', 'MCD(' + self.data['MUid'] + ')' if self.data['MUid'] else '']): ['ScanInformation', 'MCD(' + self.data['MUid'] + ')' if self.data['MUid'] else '']):
if row[1]: if row[1]:
main = doc.createElement(row[0]) main = doc.createElement(row[0])
@@ -165,5 +168,5 @@ class MetadataParser:
extracted = True extracted = True
if not extracted: if not extracted:
rmtree(workdir) rmtree(workdir)
raise OSError raise OSError('Failed to modify 7ZIP file.')
rmtree(workdir) rmtree(workdir)

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

@@ -17,18 +17,20 @@
# #
import os import os
from sys import version_info
from hashlib import md5 from hashlib import md5
from html.parser import HTMLParser from html.parser import HTMLParser
from distutils.version import StrictVersion from distutils.version import StrictVersion
from time import sleep from time import sleep
from shutil import rmtree, move from shutil import rmtree, move, copy
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:
walk = None walk = os.walk
class HTMLStripper(HTMLParser): class HTMLStripper(HTMLParser):
@@ -45,6 +47,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)
@@ -112,21 +117,27 @@ def removeFromZIP(zipfname, *filenames):
for item in zipread.infolist(): for item in zipread.infolist():
if item.filename not in filenames: if item.filename not in filenames:
zipwrite.writestr(item, zipread.read(item.filename)) zipwrite.writestr(item, zipread.read(item.filename))
move(tempname, zipfname) copy(tempname, zipfname)
finally: finally:
rmtree(tempdir) rmtree(tempdir, True)
def sanitizeTrace(traceback):
return ''.join(format_tb(traceback))\
.replace('C:\\Users\\pawel\\Documents\\Projekty\\KCC\\', '')\
.replace('C:\\Python34\\', '')\
.replace('C:\\Python34_64\\', '')
# noinspection PyUnresolvedReferences
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.2.1') > StrictVersion(qtVersion()):
missing.append('PyQt 5.2.0+') missing.append('PyQt 5.2.1+')
except ImportError: except ImportError:
missing.append('PyQt 5.2.0+') missing.append('PyQt 5.2.1+')
if level > 1: if level > 1:
try: try:
from psutil import __version__ as psutilVersion from psutil import __version__ as psutilVersion
@@ -136,22 +147,23 @@ 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):
missing.append('Pillow 2.8.2+') missing.append('Pillow 2.8.2+')
except ImportError: except ImportError:
missing.append('Pillow 2.8.2+') missing.append('Pillow 2.8.2+')
try: if version_info[1] < 5:
from scandir import __version__ as scandirVersion try:
if StrictVersion('1.1') > StrictVersion(scandirVersion): from scandir import __version__ as scandirVersion
if StrictVersion('1.1') > StrictVersion(scandirVersion):
missing.append('scandir 1.1+')
except ImportError:
missing.append('scandir 1.1+') missing.append('scandir 1.1+')
except ImportError:
missing.append('scandir 1.1+')
if len(missing) > 0: if len(missing) > 0:
print('ERROR: ' + ', '.join(missing) + ' is not installed!') print('ERROR: ' + ', '.join(missing) + ' is not installed!')
exit(1) exit(1)

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

@@ -11,21 +11,20 @@ Usage (Linux):
Usage (Mac OS X): Usage (Mac OS X):
python3 setup.py py2app python3 setup.py py2app
""" """
from sys import platform, version_info, argv from sys import platform, version_info, argv
from kcc import __version__ from kcc import __version__
if version_info[0] != 3:
print('ERROR: This is Python 3 script!')
exit(1)
NAME = 'KindleComicConverter' NAME = 'KindleComicConverter'
VERSION = __version__ 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'],
@@ -37,7 +36,7 @@ if platform == 'darwin':
includes=['sip', 'PyQt5.QtPrintSupport'], includes=['sip', 'PyQt5.QtPrintSupport'],
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',
@@ -50,6 +49,7 @@ if platform == 'darwin':
CFBundleTypeRole='Editor', CFBundleTypeRole='Editor',
) )
], ],
CFBundleIdentifier='com.kindlecomicconverter.KindleComicConverter',
LSMinimumSystemVersion='10.8.0', LSMinimumSystemVersion='10.8.0',
LSEnvironment=dict( LSEnvironment=dict(
PATH='./../Resources:/usr/local/bin:/usr/bin:/bin' PATH='./../Resources:/usr/local/bin:/usr/bin:/bin'
@@ -60,7 +60,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 +94,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,11 +136,13 @@ 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',
], ],
zip_safe=False, zip_safe=False,
) )
if version_info[1] < 5:
extra_options['install_requires'].append('scandir>=1.1.0')
setup( setup(
name=NAME, name=NAME,
@@ -156,7 +157,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')