1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-17 22:48:53 +00:00

Compare commits

...

50 Commits
4.5 ... 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
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
Paweł Jastrzębski
4c9857f14d Merge pull request #143 from ciromattia/dev
4.6
2015-06-21 08:44:37 +02:00
Paweł Jastrzębski
6b58ef4557 Updated README + version bump 2015-06-21 08:29:22 +02:00
Paweł Jastrzębski
24d697c965 Dropped Kindle Fire support 2015-06-21 08:04:28 +02:00
Paweł Jastrzębski
8b07d4eb69 Added Kindle Paperwhite 3 profile 2015-06-18 16:21:48 +02:00
Paweł Jastrzębski
e6c5ac915f Changed Kobo default output to KEPUB (close #141) 2015-06-17 22:35:24 +02:00
Paweł Jastrzębski
b22e4757a3 EPUB 3.0 output 2015-06-17 20:22:04 +02:00
Paweł Jastrzębski
91b06016bb Dependency update 2015-06-15 22:19:08 +02:00
Paweł Jastrzębski
5631391245 Error handling tweak 2015-05-31 09:36:20 +02:00
Paweł Jastrzębski
c33887b7b7 Fixed yet another tray icon anomaly 2015-05-20 20:21:12 +02:00
Paweł Jastrzębski
8d82f58f09 Merge pull request #137 from ciromattia/dev
4.5.1
2015-05-09 09:29:42 +02:00
Paweł Jastrzębski
36985f5169 Detect broken Pillow (close #135) 2015-05-07 22:37:48 +02:00
Paweł Jastrzębski
9d190c1585 Updated README + version bump 2015-05-07 18:06:16 +02:00
Paweł Jastrzębski
3834850317 Tweaked metadata editor 2015-05-07 17:49:03 +02:00
Paweł Jastrzębski
84fc23b979 Fixed CBR parsing anomalies (close #133) 2015-04-27 18:50:14 +02:00
Paweł Jastrzębski
77748afdbd Fixed supid typo 2015-04-26 18:47:40 +02:00
Paweł Jastrzębski
431e2ffaf2 Binary blob cleanup 2015-04-26 15:59:43 +02:00
Paweł Jastrzębski
16df4cd083 Added Kobo Glow HD profile 2015-04-22 20:38:17 +02:00
Paweł Jastrzębski
1aa34347c1 Added page-progression-direction tag to EPUB spine 2015-03-17 11:02:28 +01:00
Paweł Jastrzębski
561af90b06 Updated README and Setup 2015-03-09 20:08:48 +01:00
29 changed files with 773 additions and 557 deletions

105
README.md
View File

@@ -1,7 +1,7 @@
# KCC # KCC
**Kindle Comic Converter** is a Python app to convert comic/manga files or folders to EPUB, Panel View MOBI or E-Ink optimized CBZ. **Kindle Comic Converter** is a Python app to convert comic/manga files or folders to EPUB, Panel View MOBI or E-Ink optimized CBZ.
It was initially developed for Kindle but since version 2.2 it outputs valid EPUB 2.0 so _**despite its name, KCC is It was initially developed for Kindle but since version 4.6 it outputs valid EPUB 3.0 so _**despite its name, KCC is
actually a comic/manga to EPUB converter that every e-reader owner can happily use**_. actually a comic/manga to EPUB converter that every e-reader owner can happily use**_.
It can also optionally optimize images by applying a number of transformations. It can also optionally optimize images by applying a number of transformations.
@@ -29,6 +29,26 @@ You can find the latest released binary at the following links:
- **Linux:** [http://kcc.iosphe.re/Linux/](http://kcc.iosphe.re/Linux/) - **Linux:** [http://kcc.iosphe.re/Linux/](http://kcc.iosphe.re/Linux/)
- **OS X (10.8+):** [http://kcc.iosphe.re/OSX/](http://kcc.iosphe.re/OSX/) - **OS X (10.8+):** [http://kcc.iosphe.re/OSX/](http://kcc.iosphe.re/OSX/)
## DEPENDENCIES
Following software is required to run Linux version of **KCC** and/or bare sources:
- Python 3.3+
- [PyQt](http://www.riverbankcomputing.co.uk/software/pyqt/download5) 5.4.0+
- [Pillow](http://pypi.python.org/pypi/Pillow/) 2.8.2+
- [psutil](https://pypi.python.org/pypi/psutil) 3.0.0+
- [python-slugify](http://pypi.python.org/pypi/python-slugify) 1.1.3+
- [scandir](https://pypi.python.org/pypi/scandir) 1.1.0+
On Debian based distributions these two commands should install all needed dependencies:
```
sudo apt-get install python3 python3-dev python3-pip python3-pyqt5 libpng-dev libjpeg-dev p7zip-full unrar
sudo pip3 install pillow python-slugify psutil scandir
```
### Optional dependencies
- [KindleGen](http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1000765211) v2.9+ in a directory reachable by your _PATH_ or in _KCC_ directory *(For MOBI generation)*
- [UnRAR](http://www.rarlab.com/download.htm) *(For CBR/RAR support)*
- [7za](http://www.7-zip.org/download.html) *(For 7z/CB7 support)*
## INPUT FORMATS ## INPUT FORMATS
**KCC** can understand and convert, at the moment, the following input types: **KCC** can understand and convert, at the moment, the following input types:
- Folders containing: PNG, JPG or GIF files - Folders containing: PNG, JPG or GIF files
@@ -37,29 +57,6 @@ You can find the latest released binary at the following links:
- CB7, 7Z *(With `7za` executable)* - CB7, 7Z *(With `7za` executable)*
- PDF *(Only extracting JPG images)* - PDF *(Only extracting JPG images)*
## OPTIONAL REQUIREMENTS
- [KindleGen](http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1000765211) v2.9+ in a directory reachable by your _PATH_ or in _KCC_ directory *(For MOBI generation)*
- [UnRAR](http://www.rarlab.com/download.htm) *(For CBR/RAR support)*
- [7za](http://www.7-zip.org/download.html) *(For 7z/CB7 support)*
### For running from source:
- Python 3.3+
- [PyQt](http://www.riverbankcomputing.co.uk/software/pyqt/download5) 5.2.0+
- [Pillow](http://pypi.python.org/pypi/Pillow/) 2.7.0+
- [psutil](https://pypi.python.org/pypi/psutil) 2.0+
- [python-slugify](http://pypi.python.org/pypi/python-slugify) 0.1.0+
- [scandir](https://pypi.python.org/pypi/scandir) 0.9+
On Debian based distributions these two commands should install all dependencies:
```
sudo apt-get install python3 python3-dev python3-pip python3-pyqt5 libpng-dev libjpeg-dev p7zip-full unrar
sudo pip3 install pillow python-slugify psutil scandir
```
### For freezing code:
- Windows - [py2exe](https://pypi.python.org/pypi/py2exe) 0.9.2.2+
- OS X - [py2app](https://bitbucket.org/ronaldoussoren/py2app) 0.9.0+
## USAGE ## USAGE
Should be pretty self-explanatory. All options have detailed informations in tooltips. Should be pretty self-explanatory. All options have detailed informations in tooltips.
@@ -78,8 +75,7 @@ Options:
MAIN: MAIN:
-p PROFILE, --profile=PROFILE -p PROFILE, --profile=PROFILE
Device profile (Available options: K1, K2, K345, KDX, Device profile (Available options: K1, K2, K345, KDX,
KPW, KV, KFHD, KFHDX, KFHDX8, KFA, KoMT, KoG, KoA, KPW, KV, KoMT, KoG, KoGHD, KoA, KoAHD, KoAH2O) [Default=KV]
KoAHD, KoAH2O) [Default=KV]
-q QUALITY, --quality=QUALITY -q QUALITY, --quality=QUALITY
Quality of Panel View. 0 - Normal 1 - High 2 - Ultra [Default=0] Quality of Panel View. 0 - Normal 1 - High 2 - Ultra [Default=0]
-m, --manga-style Manga style (Right-to-left reading and splitting) -m, --manga-style Manga style (Right-to-left reading and splitting)
@@ -148,19 +144,52 @@ The app relies and includes the following scripts:
- Icon is by **Nikolay Verin** ([http://ncrow.deviantart.com/](http://ncrow.deviantart.com/)) and released under [CC BY-NC-SA 3.0](http://creativecommons.org/licenses/by-nc-sa/3.0/) License. - Icon is by **Nikolay Verin** ([http://ncrow.deviantart.com/](http://ncrow.deviantart.com/)) and released under [CC BY-NC-SA 3.0](http://creativecommons.org/licenses/by-nc-sa/3.0/) License.
## SAMPLE FILES CREATED BY KCC ## SAMPLE FILES CREATED BY KCC
* [Kindle Voyage](http://kcc.iosphe.re/Samples/Ubunchu!-KV.mobi) * [Kindle Paperwhite 3 / Voyage](http://kcc.iosphe.re/Samples/Ubunchu!-KV.mobi)
* [Kindle Paperwhite](http://kcc.iosphe.re/Samples/Ubunchu!-KPW.mobi) * [Kindle Paperwhite 1 / 2](http://kcc.iosphe.re/Samples/Ubunchu!-KPW.mobi)
* [Kindle](http://kcc.iosphe.re/Samples/Ubunchu!-K345.mobi) * [Kindle](http://kcc.iosphe.re/Samples/Ubunchu!-K345.mobi)
* [Kindle DX/DXG](http://kcc.iosphe.re/Samples/Ubunchu!-KDX.cbz) * [Kindle DX/DXG](http://kcc.iosphe.re/Samples/Ubunchu!-KDX.cbz)
* [Kobo Mini/Touch](http://kcc.iosphe.re/Samples/Ubunchu!-KoMT.cbz) * [Kobo Mini/Touch](http://kcc.iosphe.re/Samples/Ubunchu-KoMT.kepub.epub)
* [Kobo Glow](http://kcc.iosphe.re/Samples/Ubunchu!-KoG.cbz) * [Kobo Glo](http://kcc.iosphe.re/Samples/Ubunchu-KoG.kepub.epub)
* [Kobo Aura](http://kcc.iosphe.re/Samples/Ubunchu!-KoA.cbz) * [Kobo Glo HD](http://kcc.iosphe.re/Samples/Ubunchu-KoGHD.kepub.epub)
* [Kobo Aura HD](http://kcc.iosphe.re/Samples/Ubunchu!-KoAHD.cbz) * [Kobo Aura](http://kcc.iosphe.re/Samples/Ubunchu-KoA.kepub.epub)
* [Kobo Aura H2O](http://kcc.iosphe.re/Samples/Ubunchu!-KoAH2O.cbz) * [Kobo Aura HD](http://kcc.iosphe.re/Samples/Ubunchu-KoAHD.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:
* 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:
* KEPUB is now default output for all Kobo profiles
* EPUB output now produce fully valid EPUB 3.0.1
* Added profile for Kindle Paperwhite 3
* Dropped official support of all Kindle Fire models and Kindle for Android
* Other minor tweaks
####4.5.1:
* Added Kobo Glo HD profile
* Fixed RAR/CBR parsing anomalies
* Minor bug fixes and tweaks
####4.5: ####4.5:
* Added simple ComicRack medadata editor * Added simple ComicRack metadata editor
* Re-enabled Manga Cover Database support * Re-enabled Manga Cover Database support
* ComicRack bookmarks are now parsed * ComicRack bookmarks are now parsed
* Fixed glitches in Kindle Voyage profile * Fixed glitches in Kindle Voyage profile
@@ -404,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

@@ -34,4 +34,4 @@ if __name__ == "__main__":
freeze_support() freeze_support()
print('comic2ebook v' + __version__ + ' - Written by Ciro Mattia Gonano and Pawel Jastrzebski.') print('comic2ebook v' + __version__ + ' - Written by Ciro Mattia Gonano and Pawel Jastrzebski.')
main(sys.argv[1:]) main(sys.argv[1:])
sys.exit(0) sys.exit(0)

View File

@@ -34,4 +34,4 @@ if __name__ == "__main__":
freeze_support() freeze_support()
print('comic2panel v' + __version__ + ' - Written by Ciro Mattia Gonano and Pawel Jastrzebski.') print('comic2panel v' + __version__ + ' - Written by Ciro Mattia Gonano and Pawel Jastrzebski.')
main(sys.argv[1:]) main(sys.argv[1:])
sys.exit(0) sys.exit(0)

View File

@@ -1,5 +1,5 @@
#define MyAppName "Kindle Comic Converter" #define MyAppName "Kindle Comic Converter"
#define MyAppVersion "4.5" #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)
@@ -375,9 +368,6 @@ class WorkerThread(QtCore.QThread):
elif GUI.QualityBox.checkState() == 2: elif GUI.QualityBox.checkState() == 2:
options.quality = 2 options.quality = 2
options.format = str(GUI.FormatBox.currentText()) options.format = str(GUI.FormatBox.currentText())
if GUI.currentMode == 1:
if 'KFH' in profile:
options.upscale = True
# Advanced mode settings # Advanced mode settings
if GUI.currentMode > 1: if GUI.currentMode > 1:
@@ -440,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):
@@ -464,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'
@@ -503,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))
@@ -523,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)
@@ -539,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))
@@ -550,16 +544,17 @@ 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)
class SystemTrayIcon(QtWidgets.QSystemTrayIcon): class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
def __init__(self): def __init__(self):
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):
@@ -635,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_()
@@ -845,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:
@@ -913,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'))
@@ -930,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,
@@ -1028,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()
@@ -1161,63 +1183,53 @@ class KCCGUI(KCC_ui.Ui_KCC):
self.p.ionice(1) self.p.ionice(1)
self.profiles = { self.profiles = {
"Kindle Voyage": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 0, "K. PW 3/Voyage": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': False, 'Label': 'KV'}, 'DefaultUpscale': False, 'Label': 'KV'},
"Kindle Paperwhite": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 0, "Kindle PW 1/2": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': False, 'Label': 'KPW'}, 'DefaultUpscale': False, 'Label': 'KPW'},
"Kindle": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 0, "Kindle": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': False, 'Label': 'K345'}, 'DefaultUpscale': False, 'Label': 'K345'},
"Kindle DX/DXG": {'Quality': False, 'ForceExpert': False, 'DefaultFormat': 2, "Kindle DX/DXG": {'Quality': False, 'ForceExpert': False, 'DefaultFormat': 2,
'DefaultUpscale': False, 'Label': 'KDX'}, 'DefaultUpscale': False, 'Label': 'KDX'},
"K. Fire HD": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 0, "Kobo Mini/Touch": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 1,
'DefaultUpscale': True, 'Label': 'KFHD'},
"K. Fire HDX": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'Label': 'KFHDX'},
"K. Fire HDX 8.9": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'Label': 'KFHDX8'},
"Kobo Mini/Touch": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 2,
'DefaultUpscale': False, 'Label': 'KoMT'}, 'DefaultUpscale': False, 'Label': 'KoMT'},
"Kobo Glow": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 2, "Kobo Glo": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 1,
'DefaultUpscale': False, 'Label': 'KoG'}, 'DefaultUpscale': False, 'Label': 'KoG'},
"Kobo Aura": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 2, "Kobo Glo HD": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 1,
'DefaultUpscale': False, 'Label': 'KoGHD'},
"Kobo Aura": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 1,
'DefaultUpscale': False, 'Label': 'KoA'}, 'DefaultUpscale': False, 'Label': 'KoA'},
"Kobo Aura HD": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 2, "Kobo Aura HD": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 1,
'DefaultUpscale': False, 'Label': 'KoAHD'}, 'DefaultUpscale': False, 'Label': 'KoAHD'},
"Kobo Aura H2O": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 2, "Kobo Aura H2O": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 1,
'DefaultUpscale': False, 'Label': 'KoAH2O'}, 'DefaultUpscale': False, 'Label': 'KoAH2O'},
"Other": {'Quality': False, 'ForceExpert': True, 'DefaultFormat': 1, "Other": {'Quality': False, 'ForceExpert': True, 'DefaultFormat': 1,
'DefaultUpscale': False, 'Label': 'OTHER'}, 'DefaultUpscale': False, 'Label': 'OTHER'},
"Kindle for Android": {'Quality': False, 'ForceExpert': True, 'DefaultFormat': 0,
'DefaultUpscale': False, 'Label': 'KFA'},
"Kindle 1": {'Quality': False, 'ForceExpert': False, 'DefaultFormat': 0, "Kindle 1": {'Quality': False, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': False, 'Label': 'K1'}, 'DefaultUpscale': False, 'Label': 'K1'},
"Kindle 2": {'Quality': False, 'ForceExpert': False, 'DefaultFormat': 0, "Kindle 2": {'Quality': False, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': False, 'Label': 'K2'} 'DefaultUpscale': False, 'Label': 'K2'}
} }
profilesGUI = [ profilesGUI = [
"Kindle Voyage", "K. PW 3/Voyage",
"Kindle Paperwhite", "Kindle PW 1/2",
"Kindle", "Kindle",
"Separator", "Separator",
"K. Fire HD",
"K. Fire HDX",
"K. Fire HDX 8.9",
"Separator",
"Kobo Mini/Touch", "Kobo Mini/Touch",
"Kobo Glow", "Kobo Glo",
"Kobo Glo HD",
"Kobo Aura", "Kobo Aura",
"Kobo Aura HD", "Kobo Aura HD",
"Kobo Aura H2O", "Kobo Aura H2O",
"Separator", "Separator",
"Other", "Other",
"Separator", "Separator",
"Kindle for Android",
"Kindle 1", "Kindle 1",
"Kindle 2", "Kindle 2",
"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>')
@@ -1285,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:
@@ -1319,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_()
@@ -1339,6 +1357,8 @@ class KCCGUI_MetaEditor(KCC_MetaEditor_ui.Ui_MetaEditorDialog):
field.setText(self.parser.data[field.objectName()[:-4]]) field.setText(self.parser.data[field.objectName()[:-4]])
for field in (self.WriterLine, self.PencillerLine, self.InkerLine, self.ColoristLine): for field in (self.WriterLine, self.PencillerLine, self.InkerLine, self.ColoristLine):
field.setText(', '.join(self.parser.data[field.objectName()[:-4] + 's'])) field.setText(', '.join(self.parser.data[field.objectName()[:-4] + 's']))
if self.SeriesLine.text() == '':
self.SeriesLine.setText(file.split('\\')[-1].split('.')[0])
def saveData(self): def saveData(self):
for field in (self.VolumeLine, self.NumberLine, self.MUidLine): for field in (self.VolumeLine, self.NumberLine, self.MUidLine):
@@ -1358,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.5' __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

@@ -22,6 +22,7 @@ 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
from . import rarfile from . import rarfile
from .shared import check7ZFile as is_7zfile, saferReplace from .shared import check7ZFile as is_7zfile, saferReplace
@@ -45,7 +46,7 @@ class CBxArchive:
cbzFile = ZipFile(self.origFileName) cbzFile = ZipFile(self.origFileName)
filelist = [] filelist = []
for f in cbzFile.namelist(): for f in cbzFile.namelist():
if f.startswith('__MACOSX') or f.endswith('.DS_Store') or f.endswith('thumbs.db'): if f.startswith('__MACOSX') or f.endswith('.DS_Store') or f.endswith('humbs.db'):
pass # skip MacOS special files pass # skip MacOS special files
elif f.endswith('/'): elif f.endswith('/'):
try: try:
@@ -58,26 +59,19 @@ class CBxArchive:
def extractCBR(self, targetdir): def extractCBR(self, targetdir):
cbrFile = rarfile.RarFile(self.origFileName) cbrFile = rarfile.RarFile(self.origFileName)
filelist = [] cbrFile.extractall(targetdir)
for f in cbrFile.namelist(): for root, dirnames, filenames in walk(targetdir):
if f.startswith('__MACOSX') or f.endswith('.DS_Store') or f.endswith('thumbs.db'): for filename in filenames:
pass # skip MacOS special files if filename.startswith('__MACOSX') or filename.endswith('.DS_Store') or filename.endswith('humbs.db'):
elif f.endswith('/'): os.remove(os.path.join(root, filename))
try:
os.makedirs(os.path.join(targetdir, f))
except Exception:
pass # the dir exists so we are going to extract the images only.
else:
filelist.append(f)
cbrFile.extractall(targetdir, filelist)
def extractCB7(self, targetdir): def extractCB7(self, targetdir):
# Workaround for some wide UTF-8 + Popen abnormalities # Workaround for some wide UTF-8 + Popen abnormalities
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 -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

@@ -20,6 +20,7 @@
import os import os
import sys import sys
from time import strftime, gmtime
from copy import copy from copy import copy
from glob import glob from glob import glob
from json import loads from json import loads
@@ -27,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
@@ -37,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
@@ -64,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:
@@ -119,83 +122,98 @@ def buildHTML(path, imgfile, imgfilepath, forcePV=False):
os.makedirs(htmlpath) os.makedirs(htmlpath)
htmlfile = os.path.join(htmlpath, filename[0] + '.html') htmlfile = os.path.join(htmlpath, filename[0] + '.html')
f = open(htmlfile, "w", encoding='UTF-8') f = open(htmlfile, "w", encoding='UTF-8')
f.writelines(["<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" ", if options.iskindle:
"\"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n", f.writelines(["<?xml version=\"1.0\" encoding=\"utf-8\"?>\n",
"<html xmlns=\"http://www.w3.org/1999/xhtml\">\n", "<!DOCTYPE html>\n",
"<head>\n", "<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n",
"<title>", filename[0], "</title>\n", "<head>\n",
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n", "<title>", filename[0], "</title>\n",
"<link href=\"", "../" * (backref - 1), "<link href=\"", "../" * (backref - 1), "style.css\" type=\"text/css\" rel=\"stylesheet\"/>\n",
"style.css\" type=\"text/css\" rel=\"stylesheet\"/>\n", "<meta charset=\"utf-8\"/>\n",
"</head>\n", "</head>\n",
"<body" + additionalStyle + ">\n", "<body" + additionalStyle + ">\n",
"<div class=\"fs\">\n", "<div class=\"fs\">\n",
"<div><img src=\"", "../" * backref, "Images/", postfix, imgfile, "\" alt=\"", "<div><img src=\"", "../" * backref, "Images/", postfix, imgfile, "\" alt=\"",
imgfile, "\" class=\"singlePage\"/></div>\n" imgfile, "\" class=\"singlePage\"/></div>\n"
]) ])
if (options.panelview or forcePV) and not noPV: if (options.panelview or forcePV) and not noPV:
options.panelviewused = True options.panelviewused = True
if not noHorizontalPV and not noVerticalPV: if not noHorizontalPV and not noVerticalPV:
if rotatedPage: if rotatedPage:
if options.righttoleft: if options.righttoleft:
order = [1, 3, 2, 4] order = [1, 3, 2, 4]
else:
order = [2, 4, 1, 3]
else: else:
order = [2, 4, 1, 3] if options.righttoleft:
else: order = [2, 1, 4, 3]
if options.righttoleft: else:
order = [2, 1, 4, 3] order = [1, 2, 3, 4]
else: boxes = ["BoxTL", "BoxTR", "BoxBL", "BoxBR"]
order = [1, 2, 3, 4] elif noHorizontalPV and not noVerticalPV:
boxes = ["BoxTL", "BoxTR", "BoxBL", "BoxBR"] if rotatedPage:
elif noHorizontalPV and not noVerticalPV: if options.righttoleft:
if rotatedPage: order = [1, 2]
if options.righttoleft: else:
order = [1, 2] order = [2, 1]
else:
order = [2, 1]
else:
order = [1, 2]
boxes = ["BoxT", "BoxB"]
elif not noHorizontalPV and noVerticalPV:
if rotatedPage:
order = [1, 2]
else:
if options.righttoleft:
order = [2, 1]
else: else:
order = [1, 2] order = [1, 2]
boxes = ["BoxL", "BoxR"] boxes = ["BoxT", "BoxB"]
else: elif not noHorizontalPV and noVerticalPV:
order = [1] if rotatedPage:
boxes = ["BoxC"] order = [1, 2]
for i in range(0, len(boxes)): else:
f.writelines(["<div id=\"" + boxes[i] + "\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=", if options.righttoleft:
"'{\"targetId\":\"" + boxes[i] + "-Panel-Parent\", \"ordinal\":" + str(order[i]), order = [2, 1]
"}'></a></div>\n"]) else:
if options.quality == 2 and not forcePV: order = [1, 2]
imgfilepv = imgfile.split(".") boxes = ["BoxL", "BoxR"]
imgfilepv[0] += "-hq" else:
imgfilepv = ".".join(imgfilepv) order = [1]
else: boxes = ["BoxC"]
imgfilepv = imgfile for i in range(0, len(boxes)):
xl, yu, xr, yd = detectMargins(imgfilepath) f.writelines(["<div id=\"" + boxes[i] + "\"><a class=\"app-amzn-magnify\" data-app-amzn-magnify=",
boxStyles = {"BoxTL": "left:" + xl + ";top:" + yu + ";", "'{\"targetId\":\"" + boxes[i] + "-Panel-Parent\", \"ordinal\":" + str(order[i]),
"BoxTR": "right:" + xr + ";top:" + yu + ";", "}'></a></div>\n"])
"BoxBL": "left:" + xl + ";bottom:" + yd + ";", if options.quality == 2 and not forcePV:
"BoxBR": "right:" + xr + ";bottom:" + yd + ";", imgfilepv = imgfile.split(".")
"BoxT": "left:-25%;top:" + yu + ";", imgfilepv[0] += "-hq"
"BoxB": "left:-25%;bottom:" + yd + ";", imgfilepv = ".".join(imgfilepv)
"BoxL": "left:" + xl + ";top:-25%;", else:
"BoxR": "right:" + xr + ";top:-25%;", imgfilepv = imgfile
"BoxC": "left:-25%;top:-25%;" xl, yu, xr, yd = detectMargins(imgfilepath)
} boxStyles = {"BoxTL": "left:" + xl + ";top:" + yu + ";",
for box in boxes: "BoxTR": "right:" + xr + ";top:" + yu + ";",
f.writelines(["<div id=\"" + box + "-Panel-Parent\" class=\"target-mag-parent\"><div id=\"", "BoxBL": "left:" + xl + ";bottom:" + yd + ";",
"Generic-Panel\" class=\"target-mag\"><img style=\"" + boxStyles[box] + "\" src=\"", "BoxBR": "right:" + xr + ";bottom:" + yd + ";",
"../" * backref, "Images/", postfix, imgfilepv, "\" alt=\"" + imgfilepv, "BoxT": "left:-25%;top:" + yu + ";",
"\"/></div></div>\n", "BoxB": "left:-25%;bottom:" + yd + ";",
]) "BoxL": "left:" + xl + ";top:-25%;",
f.writelines(["</div>\n</body>\n</html>"]) "BoxR": "right:" + xr + ";top:-25%;",
"BoxC": "left:-25%;top:-25%;"
}
for box in boxes:
f.writelines(["<div id=\"" + box + "-Panel-Parent\" class=\"target-mag-parent\"><div id=\"",
"Generic-Panel\" class=\"target-mag\"><img style=\"" + boxStyles[box] + "\" src=\"",
"../" * backref, "Images/", postfix, imgfilepv, "\" alt=\"" + imgfilepv,
"\"/></div></div>\n",
])
f.writelines(["</div>\n</body>\n</html>"])
else:
f.writelines(["<?xml version=\"1.0\" encoding=\"utf-8\"?>\n",
"<!DOCTYPE html>\n",
"<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n",
"<head>\n",
"<title>", filename[0], "</title>\n",
"<link href=\"", "../" * (backref - 1), "style.css\" type=\"text/css\" rel=\"stylesheet\"/>\n",
"<meta charset=\"utf-8\"/>\n",
"</head>\n",
"<body" + additionalStyle + ">\n",
"<div class=\"epub:type=bodymatter\">\n",
"<img src=\"", "../" * backref, "Images/", postfix, imgfile, "\"/>\n",
"</div>\n",
"</body>\n</html>"
])
f.close() f.close()
return path, imgfile return path, imgfile
@@ -205,11 +223,9 @@ def buildNCX(dstdir, title, chapters, chapterNames):
ncxfile = os.path.join(dstdir, 'OEBPS', 'toc.ncx') ncxfile = os.path.join(dstdir, 'OEBPS', 'toc.ncx')
f = open(ncxfile, "w", encoding='UTF-8') f = open(ncxfile, "w", encoding='UTF-8')
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
"<!DOCTYPE ncx PUBLIC \"-//NISO//DTD ncx 2005-1//EN\" ",
"\"http://www.daisy.org/z3986/2005/ncx-2005-1.dtd\">\n",
"<ncx version=\"2005-1\" xml:lang=\"en-US\" xmlns=\"http://www.daisy.org/z3986/2005/ncx/\">\n", "<ncx version=\"2005-1\" xml:lang=\"en-US\" xmlns=\"http://www.daisy.org/z3986/2005/ncx/\">\n",
"<head>\n", "<head>\n",
"<meta name=\"dtb:uid\" content=\"", options.uuid, "\"/>\n", "<meta name=\"dtb:uid\" content=\"urn:uuid:", options.uuid, "\"/>\n",
"<meta name=\"dtb:depth\" content=\"1\"/>\n", "<meta name=\"dtb:depth\" content=\"1\"/>\n",
"<meta name=\"dtb:totalPageCount\" content=\"0\"/>\n", "<meta name=\"dtb:totalPageCount\" content=\"0\"/>\n",
"<meta name=\"dtb:maxPageNumber\" content=\"0\"/>\n", "<meta name=\"dtb:maxPageNumber\" content=\"0\"/>\n",
@@ -227,13 +243,51 @@ 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()
def buildNAV(dstdir, title, chapters, chapterNames):
navfile = os.path.join(dstdir, 'OEBPS', 'nav.xhtml')
f = open(navfile, "w", encoding='UTF-8')
f.writelines(["<?xml version=\"1.0\" encoding=\"utf-8\"?>\n",
"<!DOCTYPE html>\n",
"<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n",
"<head>\n",
"<title>" + title + "</title>\n",
"<meta charset=\"utf-8\"/>\n",
"</head>\n",
"<body>\n",
"<nav xmlns:epub=\"http://www.idpf.org/2007/ops\" epub:type=\"toc\" id=\"toc\">\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 epub:type=\"page-list\">\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.write("</ol>\n</nav>\n</body>\n</html>")
f.close()
def buildOPF(dstdir, title, filelist, cover=None): def buildOPF(dstdir, title, filelist, cover=None):
opffile = os.path.join(dstdir, 'OEBPS', 'content.opf') opffile = os.path.join(dstdir, 'OEBPS', 'content.opf')
profilelabel, deviceres, palette, gamma, panelviewsize = options.profileData profilelabel, deviceres, palette, gamma, panelviewsize = options.profileData
@@ -243,60 +297,68 @@ def buildOPF(dstdir, title, filelist, cover=None):
writingmode = "horizontal-lr" writingmode = "horizontal-lr"
f = open(opffile, "w", encoding='UTF-8') f = open(opffile, "w", encoding='UTF-8')
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
"<package version=\"2.0\" unique-identifier=\"BookID\" ", "<package version=\"3.0\" unique-identifier=\"BookID\" ",
"prefix=\"rendition: http://www.idpf.org/vocab/rendition/#\" ",
"xmlns=\"http://www.idpf.org/2007/opf\">\n", "xmlns=\"http://www.idpf.org/2007/opf\">\n",
"<metadata xmlns:opf=\"http://www.idpf.org/2007/opf\" ", "<metadata xmlns:opf=\"http://www.idpf.org/2007/opf\" ",
"xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n", "xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n",
"<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\" opf:scheme=\"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: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 name=\"generator\" content=\"KindleComicConverter-" + __version__ + "\"/>\n", f.writelines(["<meta property=\"dcterms:modified\">" + strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + "</meta>\n",
"<meta name=\"RegionMagnification\" content=\"true\"/>\n",
"<meta name=\"region-mag\" content=\"true\"/>\n",
"<meta name=\"cover\" content=\"cover\"/>\n", "<meta name=\"cover\" content=\"cover\"/>\n",
"<meta name=\"book-type\" content=\"comic\"/>\n", "<meta property=\"rendition:orientation\">portrait</meta>\n",
"<meta name=\"rendition:layout\" content=\"pre-paginated\"/>\n", "<meta property=\"rendition:spread\">portrait</meta>\n",
"<meta name=\"zero-gutter\" content=\"true\"/>\n", "<meta property=\"rendition:layout\">pre-paginated</meta>\n"])
"<meta name=\"zero-margin\" content=\"true\"/>\n", if options.iskindle and options.profile != 'Custom':
"<meta name=\"fixed-layout\" content=\"true\"/>\n" f.writelines(["<meta name=\"original-resolution\" content=\"",
"<meta name=\"rendition:orientation\" content=\"portrait\"/>\n", str(deviceres[0]) + "x" + str(deviceres[1]) + "\"/>\n",
"<meta name=\"orientation-lock\" content=\"portrait\"/>\n", "<meta name=\"book-type\" content=\"comic\"/>\n",
"<meta name=\"original-resolution\" content=\"", "<meta name=\"RegionMagnification\" content=\"true\"/>\n",
str(deviceres[0]) + "x" + str(deviceres[1]), "\"/>\n", "<meta name=\"primary-writing-mode\" content=\"" + writingmode + "\"/>\n",
"<meta name=\"primary-writing-mode\" content=\"", writingmode, "\"/>\n", "<meta name=\"zero-gutter\" content=\"true\"/>\n",
"<meta name=\"ke-border-color\" content=\"#ffffff\"/>\n", "<meta name=\"zero-margin\" content=\"true\"/>\n",
"<meta name=\"ke-border-width\" content=\"0\"/>\n", "<meta name=\"ke-border-color\" content=\"#ffffff\"/>\n",
"</metadata>\n<manifest>\n<item id=\"ncx\" href=\"toc.ncx\" ", "<meta name=\"ke-border-width\" content=\"0\"/>\n"])
"media-type=\"application/x-dtbncx+xml\"/>\n"]) f.writelines(["</metadata>\n<manifest>\n<item id=\"ncx\" href=\"toc.ncx\" ",
"media-type=\"application/x-dtbncx+xml\"/>\n",
"<item id=\"nav\" href=\"nav.xhtml\" ",
"properties=\"nav\" media-type=\"application/xhtml+xml\"/>\n"])
if cover is not None: if cover is not None:
filename = getImageFileName(cover.replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\')) filename = getImageFileName(cover.replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\'))
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=\"cover\" href=\"Images/cover" + filename[1] + "\" media-type=\"" + mt + "\"/>\n") f.write("<item id=\"cover\" href=\"Images/cover" + filename[1] + "\" media-type=\"" + mt +
"\" properties=\"cover-image\"/>\n")
reflist = [] reflist = []
for path in filelist: for path in filelist:
folder = path[0].replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\').replace("\\", "/") folder = path[0].replace(os.path.join(dstdir, 'OEBPS'), '').lstrip('/').lstrip('\\\\').replace("\\", "/")
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")
f.write("</manifest>\n<spine toc=\"ncx\">\n") if options.righttoleft:
f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n")
else:
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
for entry in reflist: for entry in reflist:
f.write("<itemref idref=\"page_" + entry + "\"/>\n") f.write("<itemref idref=\"page_" + entry + "\"/>\n")
f.write("</spine>\n<guide>\n</guide>\n</package>\n") f.write("</spine>\n</package>\n")
f.close() f.close()
os.mkdir(os.path.join(dstdir, 'META-INF')) os.mkdir(os.path.join(dstdir, 'META-INF'))
f = open(os.path.join(dstdir, 'META-INF', 'container.xml'), 'w', encoding='UTF-8') f = open(os.path.join(dstdir, 'META-INF', 'container.xml'), 'w', encoding='UTF-8')
@@ -317,114 +379,126 @@ def buildEPUB(path, chapterNames, tomeNumber):
_, deviceres, _, _, panelviewsize = options.profileData _, deviceres, _, _, panelviewsize = options.profileData
os.mkdir(os.path.join(path, 'OEBPS', 'Text')) os.mkdir(os.path.join(path, 'OEBPS', 'Text'))
f = open(os.path.join(path, 'OEBPS', 'Text', 'style.css'), 'w', encoding='UTF-8') f = open(os.path.join(path, 'OEBPS', 'Text', 'style.css'), 'w', encoding='UTF-8')
# DON'T COMPRESS CSS. KINDLE WILL FAIL TO PARSE IT. if options.iskindle:
# Generic Panel View support + Margins fix for Non-Kindle devices. f.writelines(["@page {\n",
f.writelines(["@page {\n", "margin-bottom: 0;\n",
"margin-bottom: 0;\n", "margin-top: 0\n",
"margin-top: 0\n", "}\n",
"}\n", "body {\n",
"body {\n", "display: block;\n",
"display: block;\n", "margin-bottom: 0;\n",
"margin-bottom: 0;\n", "margin-left: 0;\n",
"margin-left: 0;\n", "margin-right: 0;\n",
"margin-right: 0;\n", "margin-top: 0;\n",
"margin-top: 0;\n", "padding-bottom: 0;\n",
"padding-bottom: 0;\n", "padding-left: 0;\n",
"padding-left: 0;\n", "padding-right: 0;\n",
"padding-right: 0;\n", "padding-top: 0;\n",
"padding-top: 0;\n", "text-align: left\n",
"text-align: left\n", "}\n",
"}\n", "div.fs {\n",
"div.fs {\n", "height: ", str(deviceres[1]), "px;\n",
"height: ", str(deviceres[1]), "px;\n", "width: ", str(deviceres[0]), "px;\n",
"width: ", str(deviceres[0]), "px;\n", "position: relative;\n",
"position: relative;\n", "display: block;\n",
"display: block;\n", "text-align: center\n",
"text-align: center\n", "}\n",
"}\n", "div.fs a {\n",
"div.fs a {\n", "display: block;\n",
"display: block;\n", "width : 100%;\n",
"width : 100%;\n", "height: 100%;\n",
"height: 100%;\n", "}\n",
"}\n", "div.fs div {\n",
"div.fs div {\n", "position: absolute;\n",
"position: absolute;\n", "}\n",
"}\n", "img.singlePage {\n",
"img.singlePage {\n", "position: absolute;\n",
"position: absolute;\n", "height: ", str(deviceres[1]), "px;\n",
"height: ", str(deviceres[1]), "px;\n", "width: ", str(deviceres[0]), "px;\n",
"width: ", str(deviceres[0]), "px;\n", "}\n",
"}\n", "div.target-mag-parent {\n",
"div.target-mag-parent {\n", "width:100%;\n",
"width:100%;\n", "height:100%;\n",
"height:100%;\n", "display:none;\n",
"display:none;\n", "}\n",
"}\n", "div.target-mag {\n",
"div.target-mag {\n", "position: absolute;\n",
"position: absolute;\n", "display: block;\n",
"display: block;\n", "overflow: hidden;\n",
"overflow: hidden;\n", "}\n",
"}\n", "div.target-mag img {\n",
"div.target-mag img {\n", "position: absolute;\n",
"position: absolute;\n", "height: ", str(panelviewsize[1]), "px;\n",
"height: ", str(panelviewsize[1]), "px;\n", "width: ", str(panelviewsize[0]), "px;\n",
"width: ", str(panelviewsize[0]), "px;\n", "}\n",
"}\n", "#Generic-Panel {\n",
"#Generic-Panel {\n", "top: 0;\n",
"top: 0;\n", "height: 100%;\n",
"height: 100%;\n", "width: 100%;\n",
"width: 100%;\n", "}\n",
"}\n", "#BoxC {\n",
"#BoxC {\n", "top: 0;\n",
"top: 0;\n", "height: 100%;\n",
"height: 100%;\n", "width: 100%;\n",
"width: 100%;\n", "}\n",
"}\n", "#BoxT {\n",
"#BoxT {\n", "top: 0;\n",
"top: 0;\n", "height: 50%;\n",
"height: 50%;\n", "width: 100%;\n",
"width: 100%;\n", "}\n",
"}\n", "#BoxB {\n",
"#BoxB {\n", "bottom: 0;\n",
"bottom: 0;\n", "height: 50%;\n",
"height: 50%;\n", "width: 100%;\n",
"width: 100%;\n", "}\n",
"}\n", "#BoxL {\n",
"#BoxL {\n", "left: 0;\n",
"left: 0;\n", "height: 100%;\n",
"height: 100%;\n", "width: 50%;\n",
"width: 50%;\n", "}\n",
"}\n", "#BoxR {\n",
"#BoxR {\n", "right: 0;\n",
"right: 0;\n", "height: 100%;\n",
"height: 100%;\n", "width: 50%;\n",
"width: 50%;\n", "}\n",
"}\n", "#BoxTL {\n",
"#BoxTL {\n", "top: 0;\n",
"top: 0;\n", "left: 0;\n",
"left: 0;\n", "height: 50%;\n",
"height: 50%;\n", "width: 50%;\n",
"width: 50%;\n", "}\n",
"}\n", "#BoxTR {\n",
"#BoxTR {\n", "top: 0;\n",
"top: 0;\n", "right: 0;\n",
"right: 0;\n", "height: 50%;\n",
"height: 50%;\n", "width: 50%;\n",
"width: 50%;\n", "}\n",
"}\n", "#BoxBL {\n",
"#BoxBL {\n", "bottom: 0;\n",
"bottom: 0;\n", "left: 0;\n",
"left: 0;\n", "height: 50%;\n",
"height: 50%;\n", "width: 50%;\n",
"width: 50%;\n", "}\n",
"}\n", "#BoxBR {\n",
"#BoxBR {\n", "bottom: 0;\n",
"bottom: 0;\n", "right: 0;\n",
"right: 0;\n", "height: 50%;\n",
"height: 50%;\n", "width: 50%;\n",
"width: 50%;\n", "}",
"}", ])
]) else:
f.writelines([
"@namespace epub \"http://www.idpf.org/2007/ops\";\n",
"@charset \"UTF-8\";\n",
"body {\n",
"margin: 0;\n",
"}\n",
"img {\n",
"position: absolute;\n",
"margin: 0;\n",
"z-index: 0;\n",
"height: 100%;\n",
"}"])
f.close() f.close()
for (dirpath, dirnames, filenames) in walk(os.path.join(path, 'OEBPS', 'Images')): for (dirpath, dirnames, filenames) in walk(os.path.join(path, 'OEBPS', 'Images')):
chapter = False chapter = False
@@ -443,7 +517,7 @@ def buildEPUB(path, chapterNames, tomeNumber):
image.Cover(os.path.join(filelist[-1][0], filelist[-1][1]), cover, options, tomeNumber) image.Cover(os.path.join(filelist[-1][0], filelist[-1][1]), cover, options, tomeNumber)
# Hack that force Panel View on at last one page # Hack that force Panel View on at last one page
if lastfile and not options.panelviewused and 'Ko' not in options.profile \ if lastfile and not options.panelviewused and 'Ko' not in options.profile \
and options.profile not in ['K1', 'K2', 'KDX', 'OTHER']: and options.profile not in ['K1', 'K2', 'KDX', 'Custom']:
filelist[-1] = buildHTML(lastfile[0], lastfile[1], lastfile[2], True) filelist[-1] = buildHTML(lastfile[0], lastfile[1], lastfile[2], True)
# Overwrite chapternames if tree is flat and ComicInfo.xml has bookmarks # Overwrite chapternames if tree is flat and ComicInfo.xml has bookmarks
if not chapterNames and options.chapters: if not chapterNames and options.chapters:
@@ -461,6 +535,7 @@ def buildEPUB(path, chapterNames, tomeNumber):
chapterNames[filename] = aChapter[1] chapterNames[filename] = aChapter[1]
globaldiff = pageid - (aChapter[0] + globaldiff) globaldiff = pageid - (aChapter[0] + globaldiff)
buildNCX(path, options.title, chapterlist, chapterNames) buildNCX(path, options.title, chapterlist, chapterNames)
buildNAV(path, options.title, chapterlist, chapterNames)
buildOPF(path, options.title, filelist, cover) buildOPF(path, options.title, filelist, cover)
@@ -573,51 +648,45 @@ 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-TMP-') 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-TMP-') 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 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):
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)
@@ -629,7 +698,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]
@@ -644,6 +720,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
@@ -662,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']:
@@ -670,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()
@@ -680,6 +757,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)
@@ -882,6 +961,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:
@@ -895,11 +976,24 @@ def detectCorruption(tmpPath, orgPath):
img.verify() img.verify()
img = Image.open(path) img = Image.open(path)
img.load() img.load()
except Exception: imageNumber += 1
if options.profileData[1][0] > img.size[0] and options.profileData[1][1] > img.size[1]:
imageSmaller += 1
except Exception as err:
rmtree(os.path.join(tmpPath, '..', '..'), True) rmtree(os.path.join(tmpPath, '..', '..'), True)
raise RuntimeError('Image file %s is corrupted.' % pathOrg) if 'decoder' in str(err) and 'not available' in str(err):
raise RuntimeError('Pillow was compiled without JPG and/or PNG decoder.')
else:
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):
@@ -932,7 +1026,7 @@ def detectMargins(path):
def createNewTome(): def createNewTome():
tomePathRoot = mkdtemp('', 'KCC-TMP-') tomePathRoot = mkdtemp('', 'KCC-')
tomePath = os.path.join(tomePathRoot, 'OEBPS', 'Images') tomePath = os.path.join(tomePathRoot, 'OEBPS', 'Images')
os.makedirs(tomePath) os.makedirs(tomePath)
return tomePath, tomePathRoot return tomePath, tomePathRoot
@@ -940,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
@@ -970,8 +1064,8 @@ def makeParser():
otherOptions = OptionGroup(psr, "OTHER") otherOptions = OptionGroup(psr, "OTHER")
mainOptions.add_option("-p", "--profile", action="store", dest="profile", default="KV", mainOptions.add_option("-p", "--profile", action="store", dest="profile", default="KV",
help="Device profile (Available options: K1, K2, K345, KDX, KPW, KV, KFHD, KFHDX, KFHDX8," help="Device profile (Available options: K1, K2, K345, KDX, KPW, KV, KoMT, KoG, KoGHD,"
" KFA, KoMT, KoG, KoA, KoAHD, KoAH2O) [Default=KV]") " KoA, KoAHD, KoAH2O) [Default=KV]")
mainOptions.add_option("-q", "--quality", type="int", dest="quality", default="0", mainOptions.add_option("-q", "--quality", type="int", dest="quality", default="0",
help="Quality of Panel View. 0 - Normal 1 - High 2 - Ultra [Default=0]") help="Quality of Panel View. 0 - Normal 1 - High 2 - Ultra [Default=0]")
mainOptions.add_option("-m", "--manga-style", action="store_true", dest="righttoleft", default=False, mainOptions.add_option("-m", "--manga-style", action="store_true", dest="righttoleft", default=False,
@@ -1031,14 +1125,17 @@ def checkOptions():
global options global options
options.panelview = True options.panelview = True
options.panelviewused = False options.panelviewused = False
options.iskindle = False
options.bordersColor = None options.bordersColor = None
if options.format == 'Auto': if options.format == 'Auto':
if options.profile in ['K1', 'K2', 'K345', 'KPW', 'KV', 'KFHD', 'KFHDX', 'KFHDX8', 'KFA']: if options.profile in ['K1', 'K2', 'K345', 'KPW', 'KV']:
options.format = 'MOBI' options.format = 'MOBI'
elif options.profile in ['Other']: elif options.profile in ['OTHER', 'KoMT', 'KoG', 'KoGHD', 'KoA', 'KoAHD', 'KoAH2O']:
options.format = 'EPUB' options.format = 'EPUB'
elif options.profile in ['KDX', 'KoMT', 'KoG', 'KoA', 'KoAHD', 'KoAH2O']: elif options.profile in ['KDX']:
options.format = 'CBZ' options.format = 'CBZ'
if options.profile in ['K1', 'K2', 'K345', 'KPW', 'KV', 'OTHER']:
options.iskindle = True
if options.white_borders: if options.white_borders:
options.bordersColor = 'white' options.bordersColor = 'white'
if options.black_borders: if options.black_borders:
@@ -1046,11 +1143,6 @@ def checkOptions():
# Splitting MOBI is not optional # Splitting MOBI is not optional
if options.format == 'MOBI': if options.format == 'MOBI':
options.batchsplit = True options.batchsplit = True
# Disabling grayscale conversion for Kindle Fire family.
if 'KFH' in options.profile or options.forcecolor:
options.forcecolor = True
else:
options.forcecolor = False
# Older Kindle don't need higher resolution files due lack of Panel View. # Older Kindle don't need higher resolution files due lack of Panel View.
if options.profile == 'K1' or options.profile == 'K2' or options.profile == 'KDX': if options.profile == 'K1' or options.profile == 'K2' or options.profile == 'KDX':
options.quality = 0 options.quality = 0
@@ -1069,10 +1161,6 @@ def checkOptions():
# Kobo models can't use ultra quality mode # Kobo models can't use ultra quality mode
if options.quality == 2: if options.quality == 2:
options.quality = 1 options.quality = 1
# Kindle for Android profile require target resolution.
if options.profile == 'KFA' and (options.customwidth == 0 or options.customheight == 0):
print("ERROR: Kindle for Android profile require --customwidth and --customheight options!")
sys.exit(1)
# CBZ files on Kindle DX/DXG support higher resolution # CBZ files on Kindle DX/DXG support higher resolution
if options.profile == 'KDX' and options.format == 'CBZ': if options.profile == 'KDX' and options.format == 'CBZ':
options.customheight = 1200 options.customheight = 1200
@@ -1088,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]
@@ -1115,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
@@ -1123,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)
@@ -1205,7 +1309,7 @@ def makeMOBIFix(item):
mobiPath = item.replace('.epub', '.mobi') mobiPath = item.replace('.epub', '.mobi')
move(mobiPath, mobiPath + '_toclean') move(mobiPath, mobiPath + '_toclean')
try: try:
dualmetafix.DualMobiMetaFix(mobiPath + '_toclean', mobiPath, bytes(str(uuid4()), 'UTF-8')) dualmetafix.DualMobiMetaFix(mobiPath + '_toclean', mobiPath, bytes(options.uuid, 'UTF-8'))
return [True] return [True]
except Exception as err: except Exception as err:
return [False, format(err)] return [False, format(err)]
@@ -1255,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)])
@@ -297,4 +297,4 @@ def main(argv=None, qtGUI=None):
else: else:
raise UserWarning("Provided path is not a directory.") raise UserWarning("Provided path is not a directory.")
else: else:
raise UserWarning("Target height is not set.") raise UserWarning("Target height is not set.")

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
@@ -181,4 +182,4 @@ class DualMobiMetaFix:
replacesection(self.datain, datain_kf8, rec0) replacesection(self.datain, datain_kf8, rec0)
self.datain.flush() self.datain.flush()
self.datain.close() self.datain.close()

View File

@@ -82,14 +82,11 @@ class ProfileData:
'K2': ("Kindle 2", (600, 670), Palette15, 1.8, (900, 1005)), 'K2': ("Kindle 2", (600, 670), Palette15, 1.8, (900, 1005)),
'K345': ("Kindle", (600, 800), Palette16, 1.8, (900, 1200)), 'K345': ("Kindle", (600, 800), Palette16, 1.8, (900, 1200)),
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8, (1236, 1500)), 'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8, (1236, 1500)),
'KPW': ("Kindle Paperwhite", (758, 1024), Palette16, 1.8, (1137, 1536)), 'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8, (1137, 1536)),
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.8, (1608, 2172)), 'KV': ("Kindle Paperwhite 3/Voyage", (1072, 1448), Palette16, 1.8, (1608, 2172)),
'KFHD': ("K. Fire HD", (800, 1280), PalleteNull, 1.0, (1200, 1920)),
'KFHDX': ("K. Fire HDX", (1200, 1920), PalleteNull, 1.0, (1800, 2880)),
'KFHDX8': ("K. Fire HDX 8.9", (1600, 2560), PalleteNull, 1.0, (2400, 3840)),
'KFA': ("Kindle for Android", (0, 0), PalleteNull, 1.0, (0, 0)),
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.8, (900, 1200)), 'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.8, (900, 1200)),
'KoG': ("Kobo Glow", (768, 1024), Palette16, 1.8, (1152, 1536)), 'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.8, (1152, 1536)),
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.8, (1608, 2172)),
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.8, (1137, 1536)), 'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.8, (1137, 1536)),
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.8, (1620, 2160)), 'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.8, (1620, 2160)),
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.8, (1620, 2145)), 'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.8, (1620, 2145)),
@@ -151,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:
@@ -202,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:
@@ -431,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'
@@ -445,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:
@@ -516,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
@@ -64,7 +65,7 @@ class MetadataParser:
self.rawdata = parse(xml_file) self.rawdata = parse(xml_file)
elif is_7zfile(self.source): elif is_7zfile(self.source):
self.compressor = '7z' self.compressor = '7z'
workdir = mkdtemp('', 'KCC-TMP-') workdir = mkdtemp('', 'KCC-')
tmpXML = os.path.join(workdir, 'ComicInfo.xml') tmpXML = os.path.join(workdir, 'ComicInfo.xml')
output = Popen('7za e "' + self.source + '" ComicInfo.xml -o"' + workdir + '"', output = Popen('7za e "' + self.source + '" ComicInfo.xml -o"' + workdir + '"',
stdout=PIPE, stderr=STDOUT, shell=True) stdout=PIPE, stderr=STDOUT, shell=True)
@@ -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])
@@ -147,7 +150,7 @@ class MetadataParser:
with open(self.source, 'w', encoding='utf-8') as f: with open(self.source, 'w', encoding='utf-8') as f:
self.rawdata.writexml(f, encoding='utf-8') self.rawdata.writexml(f, encoding='utf-8')
else: else:
workdir = mkdtemp('', 'KCC-TMP-') workdir = mkdtemp('', 'KCC-')
tmpXML = os.path.join(workdir, 'ComicInfo.xml') tmpXML = os.path.join(workdir, 'ComicInfo.xml')
with open(tmpXML, 'w', encoding='utf-8') as f: with open(tmpXML, 'w', encoding='utf-8') as f:
self.rawdata.writexml(f, encoding='utf-8') self.rawdata.writexml(f, encoding='utf-8')

View File

@@ -29,7 +29,7 @@ class PdfJpgExtract:
self.origFileName = origFileName self.origFileName = origFileName
self.filename = os.path.splitext(origFileName) self.filename = os.path.splitext(origFileName)
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
self.path = self.filename[0] + "-KCC-TMP-" + ''.join(choice(ascii_uppercase + digits) for x in range(3)) self.path = self.filename[0] + "-KCC-" + ''.join(choice(ascii_uppercase + digits) for x in range(3))
def getPath(self): def getPath(self):
return self.path return self.path

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)
@@ -104,9 +108,9 @@ def saferReplace(old, new):
def removeFromZIP(zipfname, *filenames): def removeFromZIP(zipfname, *filenames):
tempdir = mkdtemp('', 'KCC-TMP-') tempdir = mkdtemp('', 'KCC-')
try: try:
tempname = os.path.join(tempdir, 'KCC-TMP.zip') tempname = os.path.join(tempdir, 'KCC.zip')
with ZipFile(zipfname, 'r') as zipread: with ZipFile(zipfname, 'r') as zipread:
with ZipFile(tempname, 'w', compression=ZIP_DEFLATED) as zipwrite: with ZipFile(tempname, 'w', compression=ZIP_DEFLATED) as zipwrite:
for item in zipread.infolist(): for item in zipread.infolist():
@@ -117,41 +121,47 @@ 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
if StrictVersion('2.0.0') > StrictVersion(psutilVersion): if StrictVersion('3.0.0') > StrictVersion(psutilVersion):
missing.append('psutil 2.0.0+') missing.append('psutil 3.0.0+')
except ImportError: except ImportError:
missing.append('psutil 2.0.0+') missing.append('psutil 3.0.0+')
try: try:
from slugify import __version__ as slugifyVersion from slugify import __version__ as slugifyVersion
if StrictVersion('0.1.0') > StrictVersion(slugifyVersion): if StrictVersion('1.1.3') > StrictVersion(slugifyVersion):
missing.append('python-slugify 0.1.0+') missing.append('python-slugify 1.1.3+')
except ImportError: except ImportError:
missing.append('python-slugify 0.1.0+') 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.7.0') > StrictVersion(pillowVersion): if StrictVersion('2.8.2') > StrictVersion(pillowVersion):
missing.append('Pillow 2.7.0+') missing.append('Pillow 2.8.2+')
except ImportError: except ImportError:
missing.append('Pillow 2.7.0+') missing.append('Pillow 2.8.2+')
try: try:
from scandir import __version__ as scandirVersion from scandir import __version__ as scandirVersion
if StrictVersion('0.9') > StrictVersion(scandirVersion): if StrictVersion('1.1') > StrictVersion(scandirVersion):
missing.append('scandir 0.9+') missing.append('scandir 1.1+')
except ImportError: except ImportError:
missing.append('scandir 0.9+') 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)

BIN
other/7za Executable file

Binary file not shown.

BIN
other/7za.exe Normal file

Binary file not shown.

BIN
other/UnRAR.exe Normal file

Binary file not shown.

BIN
other/unrar Executable file

Binary file not shown.

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

140
setup.py
View File

@@ -1,25 +1,31 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
py2exe/py2app build script for KCC. pip/py2exe/py2app build script for KCC.
Usage (Windows): Usage (Windows):
python setup.py py2exe py -3.4 setup.py py2exe
Usage (Linux):
python3 setup.py make_pyz or python3 setup.py install
Usage (Mac OS X): Usage (Mac OS X):
python setup.py py2app python3 setup.py py2app
""" """
from sys import platform, version_info from sys import platform, version_info, argv
from kcc import __version__ from kcc import __version__
if version_info[0] != 3: if version_info[0] != 3:
print('ERROR: This is Python 3 script!') print('ERROR: This is Python 3 script!')
exit(1) exit(1)
NAME = "KindleComicConverter" NAME = 'KindleComicConverter'
VERSION = __version__ VERSION = __version__
MAIN = "kcc.py" MAIN = 'kcc.py'
extra_options = {}
if platform == "darwin": if platform == 'darwin':
from setuptools import setup from setuptools import setup
from os import chmod, makedirs, system
from shutil import copyfile
extra_options = dict( extra_options = dict(
setup_requires=['py2app'], setup_requires=['py2app'],
app=[MAIN], app=[MAIN],
@@ -27,13 +33,13 @@ 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',
CFBundleExecutable=NAME, CFBundleExecutable=NAME,
CFBundleDocumentTypes=[ CFBundleDocumentTypes=[
dict( dict(
@@ -52,12 +58,11 @@ if platform == "darwin":
) )
) )
) )
elif platform == "win32": elif platform == 'win32':
# noinspection PyUnresolvedReferences
import py2exe import py2exe
import platform from platform import architecture
from distutils.core import setup from distutils.core import setup
if platform.architecture()[0] == '64bit': if architecture()[0] == '64bit':
suffix = '_64' suffix = '_64'
else: else:
suffix = '' suffix = ''
@@ -70,44 +75,87 @@ elif platform == "win32":
'C:\Python34' + suffix + '\Lib\site-packages\PyQt5\libGLESv2.dll', 'C:\Python34' + suffix + '\Lib\site-packages\PyQt5\libGLESv2.dll',
'C:\Python34' + suffix + '\Lib\site-packages\PyQt5\libEGL.dll'])] 'C:\Python34' + suffix + '\Lib\site-packages\PyQt5\libEGL.dll'])]
extra_options = dict( extra_options = dict(
options={'py2exe': {"bundle_files": 1, options={'py2exe': {'bundle_files': 1,
"dist_dir": "dist" + suffix, 'dist_dir': 'dist' + suffix,
"compressed": True, 'compressed': True,
"includes": ["sip"], 'includes': ['sip'],
"excludes": ["tkinter"], 'excludes': ['tkinter'],
"optimize": 2}}, 'optimize': 2}},
windows=[{"script": MAIN, windows=[{'script': MAIN,
"dest_base": "KCC", 'dest_base': 'KCC',
"version": VERSION, 'version': VERSION,
"copyright": "Ciro Mattia Gonano, Pawel Jastrzebski © 2012-2015", 'copyright': 'Ciro Mattia Gonano, Pawel Jastrzebski © 2012-2015',
"legal_copyright": "ISC License (ISCL)", 'legal_copyright': 'ISC License (ISCL)',
"product_version": VERSION, 'product_version': VERSION,
"product_name": "Kindle Comic Converter", 'product_name': 'Kindle Comic Converter',
"file_description": "Kindle Comic Converter", 'file_description': 'Kindle Comic Converter',
"icon_resources": [(1, "icons\comic2ebook.ico")]}], 'icon_resources': [(1, 'icons\comic2ebook.ico')]}],
zipfile=None, zipfile=None,
data_files=additional_files) data_files=additional_files)
else: else:
print('Please use setup.sh to build Linux package.') if len(argv) > 1 and argv[1] == 'make_pyz':
exit() from os import system
script = '''
cp kcc.py __main__.py
zip kcc.zip __main__.py kcc/*.py
echo "#!/usr/bin/env python3" > kcc-bin
cat kcc.zip >> kcc-bin
chmod +x kcc-bin
cp kcc-c2e.py __main__.py
zip kcc-c2e.zip __main__.py kcc/*.py
echo "#!/usr/bin/env python3" > kcc-c2e-bin
cat kcc-c2e.zip >> kcc-c2e-bin
chmod +x kcc-c2e-bin
cp kcc-c2p.py __main__.py
zip kcc-c2p.zip __main__.py kcc/*.py
echo "#!/usr/bin/env python3" > kcc-c2p-bin
cat kcc-c2p.zip >> kcc-c2p-bin
chmod +x kcc-c2p-bin
tar --xform s:^.*/:: --xform s/LICENSE.txt/LICENSE/ --xform s/kcc-bin/kcc/ --xform s/kcc-c2p-bin/kcc-c2p/ \
--xform s/kcc-c2e-bin/kcc-c2e/ --xform s/comic2ebook/kcc/ -czf KindleComicConverter_linux_'''\
+ VERSION + '''.tar.gz kcc-bin kcc-c2e-bin kcc-c2p-bin LICENSE.txt README.md icons/comic2ebook.png
rm __main__.py kcc.zip kcc-c2e.zip kcc-c2p.zip kcc-bin kcc-c2e-bin kcc-c2p-bin
'''
system("bash -c '%s'" % script)
exit(0)
else:
from setuptools import setup
from os import makedirs
from shutil import copyfile
makedirs('build/_scripts/', exist_ok=True)
copyfile('kcc.py', 'build/_scripts/kcc')
copyfile('kcc-c2e.py', 'build/_scripts/kcc-c2e')
copyfile('kcc-c2p.py', 'build/_scripts/kcc-c2p')
extra_options = dict(
scripts=['build/_scripts/kcc', 'build/_scripts/kcc-c2e', 'build/_scripts/kcc-c2p'],
packages=['kcc'],
install_requires=[
'Pillow>=2.8.2',
'psutil>=3.0.0',
'python-slugify>=1.1.3',
'scandir>=1.1.0',
],
zip_safe=False,
)
# noinspection PyUnboundLocalVariable
setup( setup(
name=NAME, name=NAME,
version=VERSION, version=VERSION,
author="Ciro Mattia Gonano, Pawel Jastrzebski", author='Ciro Mattia Gonano, Pawel Jastrzebski',
author_email="ciromattia@gmail.com, pawelj@iosphe.re", author_email='ciromattia@gmail.com, pawelj@iosphe.re',
description="Kindle Comic Converter", description='Comic and manga converter for E-Book readers.',
license="ISC License (ISCL)", license='ISC License (ISCL)',
keywords="kindle comic mobipocket mobi cbz cbr manga", keywords='kindle comic mobipocket mobi cbz cbr manga',
url="http://github.com/ciromattia/kcc", url='http://github.com/ciromattia/kcc',
**extra_options **extra_options
) )
if platform == "darwin": if platform == 'darwin':
from os import chmod, makedirs makedirs('dist/Kindle Comic Converter.app/Contents/PlugIns/platforms', exist_ok=True)
from shutil import copyfile copyfile('other/libqcocoa.dylib', 'dist/Kindle Comic Converter.app/Contents/PlugIns/platforms/libqcocoa.dylib')
makedirs('dist/' + NAME + '.app/Contents/PlugIns/platforms') chmod('dist/Kindle Comic Converter.app/Contents/Resources/unrar', 0o777)
copyfile('other/libqcocoa.dylib', 'dist/' + NAME + '.app/Contents/PlugIns/platforms/libqcocoa.dylib') chmod('dist/Kindle Comic Converter.app/Contents/Resources/7za', 0o777)
chmod('dist/' + NAME + '.app/Contents/Resources/unrar', 0o777) system('appdmg setup.json dist/KindleComicConverter_osx_' + VERSION + '.dmg')
chmod('dist/' + NAME + '.app/Contents/Resources/7za', 0o777)

View File

@@ -1,25 +0,0 @@
#!/bin/bash
# Linux Python package build script
VERSION="4.5"
cp kcc.py __main__.py
zip kcc.zip __main__.py kcc/*.py
echo "#!/usr/bin/env python3" > kcc-bin
cat kcc.zip >> kcc-bin
chmod +x kcc-bin
cp kcc-c2e.py __main__.py
zip kcc-c2e.zip __main__.py kcc/*.py
echo "#!/usr/bin/env python3" > kcc-c2e-bin
cat kcc-c2e.zip >> kcc-c2e-bin
chmod +x kcc-c2e-bin
cp kcc-c2p.py __main__.py
zip kcc-c2p.zip __main__.py kcc/*.py
echo "#!/usr/bin/env python3" > kcc-c2p-bin
cat kcc-c2p.zip >> kcc-c2p-bin
chmod +x kcc-c2p-bin
tar --xform s:^.*/:: --xform s/kcc-bin/kcc/ --xform s/kcc-c2p-bin/kcc-c2p/ --xform s/kcc-c2e-bin/kcc-c2e/ --xform s/comic2ebook/kcc/ -czf KindleComicConverter_linux_${VERSION}.tar.gz kcc-bin kcc-c2e-bin kcc-c2p-bin LICENSE.txt icons/comic2ebook.png
rm __main__.py kcc.zip kcc-c2e.zip kcc-c2p.zip kcc-bin kcc-c2e-bin kcc-c2p-bin