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

Compare commits

..

12 Commits
4.1 ... 4.2

Author SHA1 Message Date
Paweł Jastrzębski
15a240ccea Merge pull request #103 from ciromattia/4.x
Version 4.2
2014-07-18 10:29:14 +02:00
Paweł Jastrzębski
0722ddf8b0 Updated README + version bump 2014-07-18 08:45:57 +02:00
Paweł Jastrzębski
b3159b94e7 Prevented hypothetical problems with batch processing 2014-07-18 08:24:12 +02:00
Paweł Jastrzębski
ef5207c990 Dropped Windows XP support 2014-07-10 19:11:04 +02:00
Paweł Jastrzębski
db77d89817 Fixed Other profile 2014-07-05 09:38:10 +02:00
Paweł Jastrzębski
4571fadadb Resolved problems with page order on Kobo 2014-07-04 09:59:21 +02:00
Paweł Jastrzębski
94f56238ae Corruption detection now also delete non-image files (close #99) 2014-07-03 18:16:57 +02:00
Paweł Jastrzębski
5efb5d6dbb Updated Pillow (close #95) 2014-07-03 18:16:57 +02:00
Paweł Jastrzębski
623f615dd9 Added Manga Cover Database support 2014-07-03 18:16:56 +02:00
Paweł Jastrzębski
39fbbc42b3 Made KindleGen detection more foolproof 2014-07-03 18:16:56 +02:00
Paweł Jastrzębski
99405ab8a6 Disabled ultra quality mode for CBZ format 2014-07-03 18:16:55 +02:00
Paweł Jastrzębski
aadfca8306 Code cleanup 2014-07-03 18:16:54 +02:00
12 changed files with 613 additions and 573 deletions

View File

@@ -26,7 +26,7 @@ If you find **KCC** valuable you can consider donating to the authors:
## BINARY RELEASES ## BINARY RELEASES
You can find the latest released binary at the following links: You can find the latest released binary at the following links:
- **Windows:** [http://kcc.iosphe.re/Windows/](http://kcc.iosphe.re/Windows/) - **Windows (Vista or newer):** [http://kcc.iosphe.re/Windows/](http://kcc.iosphe.re/Windows/)
- **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/)
@@ -46,7 +46,7 @@ You can find the latest released binary at the following links:
### For running from source: ### For running from source:
- Python 3.3+ - Python 3.3+
- [PyQt5](http://www.riverbankcomputing.co.uk/software/pyqt/download5) 5.2.0+ - [PyQt5](http://www.riverbankcomputing.co.uk/software/pyqt/download5) 5.2.0+
- [Pillow](http://pypi.python.org/pypi/Pillow/) 2.3.0+ - [Pillow](http://pypi.python.org/pypi/Pillow/) 2.5.0+
- [psutil](https://pypi.python.org/pypi/psutil) 2.0+ - [psutil](https://pypi.python.org/pypi/psutil) 2.0+
- [python-slugify](http://pypi.python.org/pypi/python-slugify) - [python-slugify](http://pypi.python.org/pypi/python-slugify)
@@ -354,6 +354,13 @@ The app relies and includes the following scripts/binaries:
* Fixed _No optimization_ mode * Fixed _No optimization_ mode
* Multiple small tweaks nad minor bug fixes * Multiple small tweaks nad minor bug fixes
####4.2:
* Added [Manga Cover Database](http://manga.joentjuh.nl/) support
* Officially dropped Windows XP support
* Fixed _Other_ profile
* Fixed problems with page order on stock KOBO CBZ reader
* Many other small bug fixes and tweaks
## 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

@@ -18,7 +18,7 @@
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
__version__ = '4.1' __version__ = '4.2'
__license__ = 'ISC' __license__ = 'ISC'
__copyright__ = '2012-2014, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>' __copyright__ = '2012-2014, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
@@ -40,10 +40,10 @@ except ImportError:
try: try:
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
import PIL import PIL
if tuple(map(int, ('2.3.0'.split(".")))) > tuple(map(int, (PIL.PILLOW_VERSION.split(".")))): if tuple(map(int, ('2.5.0'.split(".")))) > tuple(map(int, (PIL.PILLOW_VERSION.split(".")))):
missing.append('Pillow 2.3.0+') missing.append('Pillow 2.5.0+')
except ImportError: except ImportError:
missing.append('Pillow 2.3.0+') missing.append('Pillow 2.5.0+')
try: try:
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
import slugify import slugify
@@ -63,10 +63,10 @@ if len(missing) > 0:
exit(1) exit(1)
from multiprocessing import freeze_support from multiprocessing import freeze_support
from kcc.comic2ebook import main, Copyright from kcc.comic2ebook import main
if __name__ == "__main__": if __name__ == "__main__":
freeze_support() freeze_support()
Copyright() print(('comic2ebook v%(__version__)s. Written by Ciro Mattia Gonano and Pawel Jastrzebski.' % globals()))
main(sys.argv[1:]) main(sys.argv[1:])
sys.exit(0) sys.exit(0)

View File

@@ -18,7 +18,7 @@
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
__version__ = '4.1' __version__ = '4.2'
__license__ = 'ISC' __license__ = 'ISC'
__copyright__ = '2012-2014, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>' __copyright__ = '2012-2014, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
@@ -33,10 +33,10 @@ missing = []
try: try:
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
import PIL import PIL
if tuple(map(int, ('2.3.0'.split(".")))) > tuple(map(int, (PIL.PILLOW_VERSION.split(".")))): if tuple(map(int, ('2.5.0'.split(".")))) > tuple(map(int, (PIL.PILLOW_VERSION.split(".")))):
missing.append('Pillow 2.3.0+') missing.append('Pillow 2.5.0+')
except ImportError: except ImportError:
missing.append('Pillow 2.3.0+') missing.append('Pillow 2.5.0+')
if len(missing) > 0: if len(missing) > 0:
try: try:
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
@@ -51,10 +51,10 @@ if len(missing) > 0:
exit(1) exit(1)
from multiprocessing import freeze_support from multiprocessing import freeze_support
from kcc.comic2panel import main, Copyright from kcc.comic2panel import main
if __name__ == "__main__": if __name__ == "__main__":
freeze_support() freeze_support()
Copyright() print(('comic2ebook v%(__version__)s. Written by Ciro Mattia Gonano and Pawel Jastrzebski.' % globals()))
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.1" #define MyAppVersion "4.2"
#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"
@@ -30,6 +30,7 @@ UninstallDisplayIcon={app}\{#MyAppExeName}
ChangesAssociations=True ChangesAssociations=True
InfoAfterFile=other\InstallWarning.rtf InfoAfterFile=other\InstallWarning.rtf
SignTool=SignTool /d $q{#MyAppName}$q /du $q{#MyAppURL}$q $f SignTool=SignTool /d $q{#MyAppName}$q /du $q{#MyAppURL}$q $f
MinVersion=0,6.0
[Languages] [Languages]
Name: "english"; MessagesFile: "compiler:Default.isl" Name: "english"; MessagesFile: "compiler:Default.isl"

8
kcc.py
View File

@@ -18,7 +18,7 @@
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
__version__ = '4.1' __version__ = '4.2'
__license__ = 'ISC' __license__ = 'ISC'
__copyright__ = '2012-2014, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>' __copyright__ = '2012-2014, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
@@ -47,10 +47,10 @@ except ImportError:
try: try:
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
import PIL import PIL
if tuple(map(int, ('2.3.0'.split(".")))) > tuple(map(int, (PIL.PILLOW_VERSION.split(".")))): if tuple(map(int, ('2.5.0'.split(".")))) > tuple(map(int, (PIL.PILLOW_VERSION.split(".")))):
missing.append('Pillow 2.3.0+') missing.append('Pillow 2.5.0+')
except ImportError: except ImportError:
missing.append('Pillow 2.3.0+') missing.append('Pillow 2.5.0+')
try: try:
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
import slugify import slugify

View File

@@ -17,7 +17,7 @@
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
__version__ = '4.1' __version__ = '4.2'
__license__ = 'ISC' __license__ = 'ISC'
__copyright__ = '2012-2014, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>' __copyright__ = '2012-2014, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
@@ -38,6 +38,7 @@ from xml.dom.minidom import parse
from html.parser import HTMLParser from html.parser import HTMLParser
from psutil import virtual_memory, Popen, Process from psutil import virtual_memory, Popen, Process
from uuid import uuid4 from uuid import uuid4
from copy import copy
from .shared import md5Checksum from .shared import md5Checksum
from . import comic2ebook from . import comic2ebook
from . import dualmetafix from . import dualmetafix
@@ -196,7 +197,6 @@ class VersionThread(QtCore.QThread):
def run(self): def run(self):
try: try:
sleep(1)
XML = urlopen('http://kcc.iosphe.re/Version.php') XML = urlopen('http://kcc.iosphe.re/Version.php')
XML = parse(XML) XML = parse(XML)
except Exception: except Exception:
@@ -422,9 +422,6 @@ class WorkerThread(QtCore.QThread):
if GUI.ColorBox.isChecked(): if GUI.ColorBox.isChecked():
options.forcecolor = True options.forcecolor = True
comic2ebook.options = options
comic2ebook.checkOptions()
for i in range(GUI.JobList.count()): for i in range(GUI.JobList.count()):
# Make sure that we don't consider any system message as job to do # Make sure that we don't consider any system message as job to do
if GUI.JobList.item(i).icon().isNull(): if GUI.JobList.item(i).icon().isNull():
@@ -446,7 +443,8 @@ class WorkerThread(QtCore.QThread):
jobargv = list(argv) jobargv = list(argv)
jobargv.append(job) jobargv.append(job)
try: try:
comic2ebook.options.title = 'defaulttitle' comic2ebook.options = copy(options)
comic2ebook.checkOptions()
outputPath = comic2ebook.makeBook(job, self) outputPath = comic2ebook.makeBook(job, self)
MW.hideProgressBar.emit() MW.hideProgressBar.emit()
except UserWarning as warn: except UserWarning as warn:
@@ -493,7 +491,6 @@ class WorkerThread(QtCore.QThread):
worker.signals.result.connect(self.addResult) worker.signals.result.connect(self.addResult)
self.pool.start(worker) self.pool.start(worker)
self.pool.waitForDone() self.pool.waitForDone()
sleep(0.5)
self.kindlegenErrorCode = [0] self.kindlegenErrorCode = [0]
for errors in self.workerOutput: for errors in self.workerOutput:
if errors[0] != 0: if errors[0] != 0:
@@ -503,7 +500,6 @@ class WorkerThread(QtCore.QThread):
for item in outputPath: for item in outputPath:
if os.path.exists(item): if os.path.exists(item):
os.remove(item) os.remove(item)
sleep(1)
if os.path.exists(item.replace('.epub', '.mobi')): if os.path.exists(item.replace('.epub', '.mobi')):
os.remove(item.replace('.epub', '.mobi')) os.remove(item.replace('.epub', '.mobi'))
self.clean() self.clean()
@@ -521,7 +517,6 @@ class WorkerThread(QtCore.QThread):
worker.signals.result.connect(self.addResult) worker.signals.result.connect(self.addResult)
self.pool.start(worker) self.pool.start(worker)
self.pool.waitForDone() self.pool.waitForDone()
sleep(0.5)
for success in self.workerOutput: for success in self.workerOutput:
if not success[0]: if not success[0]:
self.errors = True self.errors = True
@@ -555,7 +550,6 @@ class WorkerThread(QtCore.QThread):
for item in outputPath: for item in outputPath:
if os.path.exists(item): if os.path.exists(item):
os.remove(item) os.remove(item)
sleep(1)
if os.path.exists(item.replace('.epub', '.mobi')): if os.path.exists(item.replace('.epub', '.mobi')):
os.remove(item.replace('.epub', '.mobi')) os.remove(item.replace('.epub', '.mobi'))
MW.addMessage.emit('KindleGen failed to create MOBI!', 'error', False) MW.addMessage.emit('KindleGen failed to create MOBI!', 'error', False)
@@ -834,6 +828,9 @@ class KCCGUI(KCC_ui.Ui_KCC):
if value == 2 and 'Kobo' in str(GUI.DeviceBox.currentText()): if value == 2 and 'Kobo' in str(GUI.DeviceBox.currentText()):
self.addMessage('Kobo devices can\'t use ultra quality mode!', 'warning') self.addMessage('Kobo devices can\'t use ultra quality mode!', 'warning')
GUI.QualityBox.setCheckState(0) GUI.QualityBox.setCheckState(0)
elif value == 2 and 'CBZ' in str(GUI.FormatBox.currentText()):
self.addMessage('CBZ format don\'t support ultra quality mode!', 'warning')
GUI.QualityBox.setCheckState(0)
def changeGamma(self, value): def changeGamma(self, value):
value = float(value) value = float(value)
@@ -861,7 +858,7 @@ class KCCGUI(KCC_ui.Ui_KCC):
GUI.AdvModeButton.setEnabled(True) GUI.AdvModeButton.setEnabled(True)
if self.currentMode == 3: if self.currentMode == 3:
self.modeBasic() self.modeBasic()
self.changeFormat() self.changeFormat(event=False)
GUI.GammaSlider.setValue(0) GUI.GammaSlider.setValue(0)
self.changeGamma(0) self.changeGamma(0)
if profile['DefaultUpscale']: if profile['DefaultUpscale']:
@@ -870,19 +867,12 @@ class KCCGUI(KCC_ui.Ui_KCC):
self.addMessage('<a href="https://github.com/ciromattia/kcc/wiki/NonKindle-devices">' self.addMessage('<a href="https://github.com/ciromattia/kcc/wiki/NonKindle-devices">'
'List of supported Non-Kindle devices.</a>', 'info') 'List of supported Non-Kindle devices.</a>', 'info')
def changeFormat(self, outputFormat=None): def changeFormat(self, outputFormat=None, event=True):
profile = GUI.profiles[str(GUI.DeviceBox.currentText())] profile = GUI.profiles[str(GUI.DeviceBox.currentText())]
if outputFormat is not None: if outputFormat is not None:
GUI.FormatBox.setCurrentIndex(outputFormat) GUI.FormatBox.setCurrentIndex(outputFormat)
else: else:
if GUI.FormatBox.count() == 3:
GUI.FormatBox.setCurrentIndex(profile['DefaultFormat']) GUI.FormatBox.setCurrentIndex(profile['DefaultFormat'])
else:
if profile['DefaultFormat'] != 0:
tmpFormat = profile['DefaultFormat'] - 1
else:
tmpFormat = 0
GUI.FormatBox.setCurrentIndex(tmpFormat)
if GUI.WebtoonBox.isChecked(): if GUI.WebtoonBox.isChecked():
GUI.MangaBox.setEnabled(False) GUI.MangaBox.setEnabled(False)
GUI.QualityBox.setEnabled(False) GUI.QualityBox.setEnabled(False)
@@ -895,6 +885,10 @@ class KCCGUI(KCC_ui.Ui_KCC):
if GUI.ProcessingBox.isChecked(): if GUI.ProcessingBox.isChecked():
GUI.QualityBox.setEnabled(False) GUI.QualityBox.setEnabled(False)
GUI.QualityBox.setChecked(False) GUI.QualityBox.setChecked(False)
if event and GUI.QualityBox.isEnabled() and 'CBZ' in str(GUI.FormatBox.currentText()) and\
GUI.QualityBox.checkState() == 2:
self.addMessage('CBZ format don\'t support ultra quality mode!', 'warning')
GUI.QualityBox.setCheckState(0)
def stripTags(self, html): def stripTags(self, html):
s = HTMLStripper() s = HTMLStripper()
@@ -973,6 +967,15 @@ class KCCGUI(KCC_ui.Ui_KCC):
self.addMessage('Target resolution is not set!', 'error') self.addMessage('Target resolution is not set!', 'error')
self.needClean = True self.needClean = True
return return
if str(GUI.FormatBox.currentText()) == 'MOBI' and not GUI.KindleGen:
self.addMessage('Cannot find <a href="http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1000765211">'
'<b>KindleGen</b></a>! MOBI conversion is not possible!', 'error')
if sys.platform.startswith('win'):
self.addMessage('Download it and place EXE in KCC directory.', 'error')
else:
self.addMessage('Download it, and place executable in /usr/local/bin directory.', 'error')
self.needClean = True
return
self.worker.start() self.worker.start()
def hideProgressBar(self): def hideProgressBar(self):
@@ -1196,7 +1199,6 @@ class KCCGUI(KCC_ui.Ui_KCC):
kindleGenExitCode = Popen('kindlegen -locale en', stdout=PIPE, stderr=STDOUT, shell=True) kindleGenExitCode = Popen('kindlegen -locale en', stdout=PIPE, stderr=STDOUT, shell=True)
if kindleGenExitCode.wait() == 0: if kindleGenExitCode.wait() == 0:
self.KindleGen = True self.KindleGen = True
formats = ['MOBI', 'EPUB', 'CBZ']
versionCheck = Popen('kindlegen -locale en', stdout=PIPE, stderr=STDOUT, shell=True) versionCheck = Popen('kindlegen -locale en', stdout=PIPE, stderr=STDOUT, shell=True)
for line in versionCheck.stdout: for line in versionCheck.stdout:
line = line.decode("utf-8") line = line.decode("utf-8")
@@ -1210,13 +1212,6 @@ class KCCGUI(KCC_ui.Ui_KCC):
break break
else: else:
self.KindleGen = False self.KindleGen = False
formats = ['EPUB', 'CBZ']
if sys.platform.startswith('win'):
self.addMessage('Cannot find <a href="http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1000765211">'
'kindlegen</a> in KCC directory! MOBI creation will be disabled.', 'warning')
else:
self.addMessage('Cannot find <a href="http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1000765211">'
'kindlegen</a> in PATH! MOBI creation will be disabled.', 'warning')
rarExitCode = Popen('unrar', stdout=PIPE, stderr=STDOUT, shell=True) rarExitCode = Popen('unrar', stdout=PIPE, stderr=STDOUT, shell=True)
rarExitCode = rarExitCode.wait() rarExitCode = rarExitCode.wait()
if rarExitCode == 0 or rarExitCode == 7: if rarExitCode == 0 or rarExitCode == 7:
@@ -1271,7 +1266,7 @@ class KCCGUI(KCC_ui.Ui_KCC):
GUI.DeviceBox.addItem(self.icons.deviceKobo, profile) GUI.DeviceBox.addItem(self.icons.deviceKobo, profile)
else: else:
GUI.DeviceBox.addItem(self.icons.deviceKindle, profile) GUI.DeviceBox.addItem(self.icons.deviceKindle, profile)
for f in formats: for f in ['MOBI', 'EPUB', 'CBZ']:
GUI.FormatBox.addItem(eval('self.icons.' + f + 'Format'), f) GUI.FormatBox.addItem(eval('self.icons.' + f + 'Format'), f)
if self.lastDevice > GUI.DeviceBox.count(): if self.lastDevice > GUI.DeviceBox.count():
self.lastDevice = 0 self.lastDevice = 0
@@ -1282,7 +1277,7 @@ class KCCGUI(KCC_ui.Ui_KCC):
GUI.DeviceBox.setCurrentIndex(self.lastDevice) GUI.DeviceBox.setCurrentIndex(self.lastDevice)
self.changeDevice() self.changeDevice()
if self.currentFormat != self.profiles[str(GUI.DeviceBox.currentText())]['DefaultFormat']: if self.currentFormat != self.profiles[str(GUI.DeviceBox.currentText())]['DefaultFormat']:
self.changeFormat(self.currentFormat) self.changeFormat(self.currentFormat, False)
for option in self.options: for option in self.options:
if str(option) == "customWidth": if str(option) == "customWidth":
GUI.customWidth.setText(str(self.options[option])) GUI.customWidth.setText(str(self.options[option]))

View File

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

View File

@@ -18,14 +18,16 @@
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
# #
__version__ = '4.1' __version__ = '4.2'
__license__ = 'ISC' __license__ = 'ISC'
__copyright__ = '2012-2014, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>' __copyright__ = '2012-2014, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os import os
import sys import sys
from re import split, sub from json import loads
from urllib.request import Request, urlopen
from re import split, sub, compile
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
@@ -47,24 +49,35 @@ from . import cbxarchive
from . import pdfjpgextract from . import pdfjpgextract
def main(argv=None):
global options
parser = makeParser()
options, args = parser.parse_args(argv)
checkOptions()
if len(args) != 1:
parser.print_help()
return
outputPath = makeBook(args[0])
return outputPath
def buildHTML(path, imgfile, imgfilepath): def buildHTML(path, imgfile, imgfilepath):
imgfilepath = md5Checksum(imgfilepath) imgfilepath = md5Checksum(imgfilepath)
filename = getImageFileName(imgfile) filename = getImageFileName(imgfile)
if filename is not None:
if options.imgproc: if options.imgproc:
if "Rotated" in theGreatIndex[imgfilepath]: if "Rotated" in options.imgIndex[imgfilepath]:
rotatedPage = True rotatedPage = True
else: else:
rotatedPage = False rotatedPage = False
if "NoPanelView" in theGreatIndex[imgfilepath]: if "NoPanelView" in options.imgIndex[imgfilepath]:
noPV = True noPV = True
else: else:
noPV = False noPV = False
if "NoHorizontalPanelView" in theGreatIndex[imgfilepath]: if "NoHorizontalPanelView" in options.imgIndex[imgfilepath]:
noHorizontalPV = True noHorizontalPV = True
else: else:
noHorizontalPV = False noHorizontalPV = False
if "NoVerticalPanelView" in theGreatIndex[imgfilepath]: if "NoVerticalPanelView" in options.imgIndex[imgfilepath]:
noVerticalPV = True noVerticalPV = True
else: else:
noVerticalPV = False noVerticalPV = False
@@ -146,7 +159,7 @@ def buildHTML(path, imgfile, imgfilepath):
imgfilepv = ".".join(imgfilepv) imgfilepv = ".".join(imgfilepv)
else: else:
imgfilepv = imgfile imgfilepv = imgfile
xl, yu, xr, yd = checkMargins(imgfilepath) xl, yu, xr, yd = detectMargins(imgfilepath)
boxStyles = {"BoxTL": "left:" + xl + ";top:" + yu + ";", boxStyles = {"BoxTL": "left:" + xl + ";top:" + yu + ";",
"BoxTR": "right:" + xr + ";top:" + yu + ";", "BoxTR": "right:" + xr + ";top:" + yu + ";",
"BoxBL": "left:" + xl + ";bottom:" + yd + ";", "BoxBL": "left:" + xl + ";bottom:" + yd + ";",
@@ -168,35 +181,6 @@ def buildHTML(path, imgfile, imgfilepath):
return path, imgfile return path, imgfile
def checkMargins(path):
if options.imgproc:
for flag in theGreatIndex[path]:
if "Margins-" in flag:
flag = flag.split('-')
xl = flag[1]
yu = flag[2]
xr = flag[3]
yd = flag[4]
if xl != "0":
xl = "-" + str(float(xl)/100) + "%"
else:
xl = "0%"
if xr != "0":
xr = "-" + str(float(xr)/100) + "%"
else:
xr = "0%"
if yu != "0":
yu = "-" + str(float(yu)/100) + "%"
else:
yu = "0%"
if yd != "0":
yd = "-" + str(float(yd)/100) + "%"
else:
yd = "0%"
return xl, yu, xr, yd
return '0%', '0%', '0%', '0%'
def buildNCX(dstdir, title, chapters, chapterNames): def buildNCX(dstdir, title, chapters, chapterNames):
options.uuid = str(uuid4()) options.uuid = str(uuid4())
ncxfile = os.path.join(dstdir, 'OEBPS', 'toc.ncx') ncxfile = os.path.join(dstdir, 'OEBPS', 'toc.ncx')
@@ -305,133 +289,7 @@ def buildOPF(dstdir, title, filelist, cover=None):
f.close() f.close()
def applyImgOptimization(img, opt, hqImage=None): def buildEPUB(path, chapterNames, tomeNumber):
if not img.fill:
img.getImageFill(opt.webtoon)
if not opt.webtoon:
img.cropWhiteSpace()
if opt.cutpagenumbers and not opt.webtoon:
img.cutPageNumber()
img.optimizeImage(opt.gamma)
if hqImage:
img.resizeImage(opt.upscale, opt.stretch, opt.bordersColor, 0)
img.calculateBorder(hqImage, True)
else:
img.resizeImage(opt.upscale, opt.stretch, opt.bordersColor, opt.quality)
if opt.panelview:
if opt.quality == 0:
img.calculateBorder(img)
elif opt.quality == 1:
img.calculateBorder(img, True)
if opt.forcepng and not opt.forcecolor:
img.quantizeImage()
def dirImgProcess(path):
global workerPool, workerOutput, theGreatIndex, theGreatWipe
workerPool = Pool()
workerOutput = []
work = []
theGreatIndex = {}
theGreatWipe = []
pagenumber = 0
for (dirpath, dirnames, filenames) in os.walk(path):
for afile in filenames:
if getImageFileName(afile) is not None:
pagenumber += 1
work.append([afile, dirpath, options])
if GUI:
GUI.progressBarTick.emit(str(pagenumber))
if len(work) > 0:
for i in work:
workerPool.apply_async(func=fileImgProcess, args=(i, ), callback=fileImgProcess_tick)
workerPool.close()
workerPool.join()
if GUI and not GUI.conversionAlive:
rmtree(os.path.join(path, '..', '..'), True)
raise UserWarning("Conversion interrupted.")
if len(workerOutput) > 0:
rmtree(os.path.join(path, '..', '..'), True)
raise RuntimeError("One of workers crashed. Cause: " + workerOutput[0])
for file in theGreatWipe:
if os.path.isfile(file):
os.remove(file)
else:
rmtree(os.path.join(path, '..', '..'), True)
raise UserWarning("Source directory is empty.")
def fileImgProcess_tick(output):
if isinstance(output, str):
workerOutput.append(output)
workerPool.terminate()
else:
for page in output:
if page is not None:
if isinstance(page, str):
theGreatWipe.append(page)
else:
theGreatIndex[page[0]] = page[1]
if GUI:
GUI.progressBarTick.emit('tick')
if not GUI.conversionAlive:
workerPool.terminate()
def fileImgProcess(work):
try:
afile = work[0]
dirpath = work[1]
opt = work[2]
output = []
img = image.ComicPage(os.path.join(dirpath, afile), opt.profileData)
if opt.quality == 2:
wipe = False
else:
wipe = True
if opt.nosplitrotate:
splitter = None
else:
splitter = img.splitPage(dirpath, opt.righttoleft, opt.rotate)
if splitter is not None:
img0 = image.ComicPage(splitter[0], opt.profileData)
applyImgOptimization(img0, opt)
output.append(img0.saveToDir(dirpath, opt.forcepng, opt.forcecolor))
img1 = image.ComicPage(splitter[1], opt.profileData)
applyImgOptimization(img1, opt)
output.append(img1.saveToDir(dirpath, opt.forcepng, opt.forcecolor))
if wipe:
output.append(img0.origFileName)
output.append(img1.origFileName)
if opt.quality == 2:
img0b = image.ComicPage(splitter[0], opt.profileData, img0.fill)
applyImgOptimization(img0b, opt, img0)
output.append(img0b.saveToDir(dirpath, opt.forcepng, opt.forcecolor))
img1b = image.ComicPage(splitter[1], opt.profileData, img1.fill)
applyImgOptimization(img1b, opt, img1)
output.append(img1b.saveToDir(dirpath, opt.forcepng, opt.forcecolor))
output.append(img0.origFileName)
output.append(img1.origFileName)
output.append(img.origFileName)
else:
applyImgOptimization(img, opt)
output.append(img.saveToDir(dirpath, opt.forcepng, opt.forcecolor))
if wipe:
output.append(img.origFileName)
if opt.quality == 2:
img2 = image.ComicPage(os.path.join(dirpath, afile), opt.profileData, img.fill)
if img.rotated:
img2.image = img2.image.rotate(90)
img2.rotated = True
applyImgOptimization(img2, opt, img)
output.append(img2.saveToDir(dirpath, opt.forcepng, opt.forcecolor))
output.append(img.origFileName)
return output
except Exception:
return str(sys.exc_info()[1])
def genEpubStruct(path, chapterNames):
filelist = [] filelist = []
chapterlist = [] chapterlist = []
cover = None cover = None
@@ -551,7 +409,7 @@ def genEpubStruct(path, chapterNames):
chapter = False chapter = False
for afile in filenames: for afile in filenames:
filename = getImageFileName(afile) filename = getImageFileName(afile)
if filename is not None and not '-kcc-hq' in filename[0]: if not '-kcc-hq' in filename[0]:
filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile))) filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile)))
if not chapter: if not chapter:
chapterlist.append((dirpath.replace('Images', 'Text'), filelist[-1][1])) chapterlist.append((dirpath.replace('Images', 'Text'), filelist[-1][1]))
@@ -559,7 +417,7 @@ def genEpubStruct(path, chapterNames):
if cover is None: if cover is None:
cover = os.path.join(os.path.join(path, 'OEBPS', 'Images'), cover = os.path.join(os.path.join(path, 'OEBPS', 'Images'),
'cover' + getImageFileName(filelist[-1][1])[1]) 'cover' + getImageFileName(filelist[-1][1])[1])
image.Cover(os.path.join(filelist[-1][0], filelist[-1][1]), cover) image.Cover(os.path.join(filelist[-1][0], filelist[-1][1]), cover, options, tomeNumber)
buildNCX(path, options.title, chapterlist, chapterNames) buildNCX(path, options.title, chapterlist, chapterNames)
# Ensure we're sorting files alphabetically # Ensure we're sorting files alphabetically
convert = lambda text: int(text) if text.isdigit() else text convert = lambda text: int(text) if text.isdigit() else text
@@ -568,6 +426,131 @@ def genEpubStruct(path, chapterNames):
buildOPF(path, options.title, filelist, cover) buildOPF(path, options.title, filelist, cover)
def imgOptimization(img, opt, hqImage=None):
if not img.fill:
img.getImageFill(opt.webtoon)
if not opt.webtoon:
img.cropWhiteSpace()
if opt.cutpagenumbers and not opt.webtoon:
img.cutPageNumber()
img.optimizeImage(opt.gamma)
if hqImage:
img.resizeImage(opt.upscale, opt.stretch, opt.bordersColor, 0)
img.calculateBorder(hqImage, True)
else:
img.resizeImage(opt.upscale, opt.stretch, opt.bordersColor, opt.quality)
if opt.panelview:
if opt.quality == 0:
img.calculateBorder(img)
elif opt.quality == 1:
img.calculateBorder(img, True)
if opt.forcepng and not opt.forcecolor:
img.quantizeImage()
def imgDirectoryProcessing(path):
global workerPool, workerOutput
workerPool = Pool()
workerOutput = []
options.imgIndex = {}
options.imgPurgeIndex = []
work = []
pagenumber = 0
for (dirpath, dirnames, filenames) in os.walk(path):
for afile in filenames:
pagenumber += 1
work.append([afile, dirpath, options])
if GUI:
GUI.progressBarTick.emit(str(pagenumber))
if len(work) > 0:
for i in work:
workerPool.apply_async(func=imgFileProcessing, args=(i, ), callback=imgFileProcessingTick)
workerPool.close()
workerPool.join()
if GUI and not GUI.conversionAlive:
rmtree(os.path.join(path, '..', '..'), True)
raise UserWarning("Conversion interrupted.")
if len(workerOutput) > 0:
rmtree(os.path.join(path, '..', '..'), True)
raise RuntimeError("One of workers crashed. Cause: " + workerOutput[0])
for file in options.imgPurgeIndex:
if os.path.isfile(file):
os.remove(file)
else:
rmtree(os.path.join(path, '..', '..'), True)
raise UserWarning("Source directory is empty.")
def imgFileProcessingTick(output):
if isinstance(output, str):
workerOutput.append(output)
workerPool.terminate()
else:
for page in output:
if page is not None:
if isinstance(page, str):
options.imgPurgeIndex.append(page)
else:
options.imgIndex[page[0]] = page[1]
if GUI:
GUI.progressBarTick.emit('tick')
if not GUI.conversionAlive:
workerPool.terminate()
def imgFileProcessing(work):
try:
afile = work[0]
dirpath = work[1]
opt = work[2]
output = []
img = image.ComicPage(os.path.join(dirpath, afile), opt.profileData)
if opt.quality == 2:
wipe = False
else:
wipe = True
if opt.nosplitrotate:
splitter = None
else:
splitter = img.splitPage(dirpath, opt.righttoleft, opt.rotate)
if splitter is not None:
img0 = image.ComicPage(splitter[0], opt.profileData)
imgOptimization(img0, opt)
output.append(img0.saveToDir(dirpath, opt.forcepng, opt.forcecolor))
img1 = image.ComicPage(splitter[1], opt.profileData)
imgOptimization(img1, opt)
output.append(img1.saveToDir(dirpath, opt.forcepng, opt.forcecolor))
if wipe:
output.append(img0.origFileName)
output.append(img1.origFileName)
if opt.quality == 2:
img0b = image.ComicPage(splitter[0], opt.profileData, img0.fill)
imgOptimization(img0b, opt, img0)
output.append(img0b.saveToDir(dirpath, opt.forcepng, opt.forcecolor))
img1b = image.ComicPage(splitter[1], opt.profileData, img1.fill)
imgOptimization(img1b, opt, img1)
output.append(img1b.saveToDir(dirpath, opt.forcepng, opt.forcecolor))
output.append(img0.origFileName)
output.append(img1.origFileName)
output.append(img.origFileName)
else:
imgOptimization(img, opt)
output.append(img.saveToDir(dirpath, opt.forcepng, opt.forcecolor))
if wipe:
output.append(img.origFileName)
if opt.quality == 2:
img2 = image.ComicPage(os.path.join(dirpath, afile), opt.profileData, img.fill)
if img.rotated:
img2.image = img2.image.rotate(90)
img2.rotated = True
imgOptimization(img2, opt, img)
output.append(img2.saveToDir(dirpath, opt.forcepng, opt.forcecolor))
output.append(img.origFileName)
return output
except Exception:
return str(sys.exc_info()[1])
def getWorkFolder(afile): def getWorkFolder(afile):
if len(afile) > 240: if len(afile) > 240:
raise UserWarning("Path is too long.") raise UserWarning("Path is too long.")
@@ -579,7 +562,7 @@ def getWorkFolder(afile):
if len(fullPath) > 240: if len(fullPath) > 240:
raise UserWarning("Path is too long.") raise UserWarning("Path is too long.")
copytree(afile, fullPath) copytree(afile, fullPath)
sanitizeTreeBeforeConversion(fullPath) sanitizePermissions(fullPath)
return workdir return workdir
except OSError: except OSError:
rmtree(workdir, True) rmtree(workdir, True)
@@ -609,9 +592,36 @@ def getWorkFolder(afile):
return path return path
def checkComicInfo(path, originalPath): def getOutputFilename(srcpath, wantedname, ext, tomeNumber):
if srcpath[-1] == os.path.sep:
srcpath = srcpath[:-1]
if not ext.startswith('.'):
ext = '.' + ext
if wantedname is not None:
if wantedname.endswith(ext):
filename = os.path.abspath(wantedname)
elif os.path.isdir(srcpath):
filename = os.path.join(os.path.abspath(options.output), os.path.basename(srcpath) + ext)
else:
filename = os.path.join(os.path.abspath(options.output),
os.path.basename(os.path.splitext(srcpath)[0]) + ext)
elif os.path.isdir(srcpath):
filename = srcpath + tomeNumber + ext
else:
filename = os.path.splitext(srcpath)[0] + tomeNumber + ext
if os.path.isfile(filename):
counter = 0
basename = os.path.splitext(filename)[0]
while os.path.isfile(basename + '_kcc' + str(counter) + ext):
counter += 1
filename = basename + '_kcc' + str(counter) + ext
return filename
def getComicInfo(path, originalPath):
xmlPath = os.path.join(path, 'ComicInfo.xml') xmlPath = os.path.join(path, 'ComicInfo.xml')
options.authors = ['KCC'] options.authors = ['KCC']
options.remoteCovers = {}
titleSuffix = '' titleSuffix = ''
if options.title == 'defaulttitle': if options.title == 'defaulttitle':
defaultTitle = True defaultTitle = True
@@ -657,22 +667,40 @@ def checkComicInfo(path, originalPath):
options.authors.sort() options.authors.sort()
else: else:
options.authors = ['KCC'] options.authors = ['KCC']
if len(xml.getElementsByTagName('ScanInformation')) != 0:
coverId = xml.getElementsByTagName('ScanInformation')[0].firstChild.nodeValue
coverId = compile('(MCD\\()(\\d+)(\\))').search(coverId)
if coverId:
options.remoteCovers = getCoversFromMCB(coverId.group(2))
os.remove(xmlPath) os.remove(xmlPath)
def slugify(value): def getCoversFromMCB(mangaID):
value = slugifyExt(value) covers = {}
value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value)) try:
return value jsonRaw = urlopen(Request('http://manga.joentjuh.nl/json/series/' + mangaID + '/',
headers={'User-Agent': 'KindleComicConverter/' + __version__}))
jsonData = loads(jsonRaw.readall().decode('utf-8'))
for volume in jsonData['volumes']:
covers[int(volume['volume'])] = volume['releases'][0]['files']['front']['url']
except Exception:
return {}
return covers
def getDirectorySize(start_path='.'):
total_size = 0
for dirpath, dirnames, filenames in os.walk(start_path):
for f in filenames:
fp = os.path.join(dirpath, f)
total_size += os.path.getsize(fp)
return total_size
def sanitizeTree(filetree): def sanitizeTree(filetree):
chapterNames = {} chapterNames = {}
for root, dirs, files in os.walk(filetree, False): for root, dirs, files in os.walk(filetree, False):
for name in files: for name in files:
if name.startswith('.') or name.lower() == 'thumbs.db':
os.remove(os.path.join(root, name))
else:
splitname = os.path.splitext(name) splitname = os.path.splitext(name)
slugified = slugify(splitname[0]) slugified = slugify(splitname[0])
while os.path.exists(os.path.join(root, slugified + splitname[1])) and splitname[0].upper()\ while os.path.exists(os.path.join(root, slugified + splitname[1])) and splitname[0].upper()\
@@ -683,9 +711,6 @@ def sanitizeTree(filetree):
if key != newKey: if key != newKey:
os.replace(key, newKey) os.replace(key, newKey)
for name in dirs: for name in dirs:
if name.startswith('.'):
os.remove(os.path.join(root, name))
else:
tmpName = name tmpName = name
slugified = slugify(name) slugified = slugify(name)
while os.path.exists(os.path.join(root, slugified)) and name.upper() != slugified.upper(): while os.path.exists(os.path.join(root, slugified)) and name.upper() != slugified.upper():
@@ -698,7 +723,25 @@ def sanitizeTree(filetree):
return chapterNames return chapterNames
def sanitizeTreeBeforeConversion(filetree): def sanitizeTreeKobo(filetree):
pageNumber = 0
for root, dirs, files in os.walk(filetree):
files.sort()
dirs.sort()
for name in files:
splitname = os.path.splitext(name)
slugified = str(pageNumber).zfill(5)
pageNumber += 1
while os.path.exists(os.path.join(root, slugified + splitname[1])) and splitname[0].upper()\
!= slugified.upper():
slugified += "A"
newKey = os.path.join(root, slugified + splitname[1])
key = os.path.join(root, name)
if key != newKey:
os.replace(key, newKey)
def sanitizePermissions(filetree):
for root, dirs, files in os.walk(filetree, False): for root, dirs, files in os.walk(filetree, False):
for name in files: for name in files:
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD) os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD)
@@ -706,23 +749,66 @@ def sanitizeTreeBeforeConversion(filetree):
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD | S_IEXEC) os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD | S_IEXEC)
def getDirectorySize(start_path='.'): #noinspection PyUnboundLocalVariable
total_size = 0 def splitDirectory(path):
for dirpath, dirnames, filenames in os.walk(start_path): # Detect directory stucture
for f in filenames: for root, dirs, files in walkLevel(os.path.join(path, 'OEBPS', 'Images'), 0):
fp = os.path.join(dirpath, f) subdirectoryNumber = len(dirs)
total_size += os.path.getsize(fp) filesNumber = len(files)
return total_size if subdirectoryNumber == 0:
# No subdirectories
mode = 0
else:
if filesNumber > 0:
print('\nWARNING: Automatic output splitting failed.')
if GUI:
GUI.addMessage.emit('Automatic output splitting failed. <a href='
'"https://github.com/ciromattia/kcc/wiki'
'/Automatic-output-splitting">'
'More details.</a>', 'warning', False)
GUI.addMessage.emit('', '', False)
return [path]
detectedSubSubdirectories = False
detectedFilesInSubdirectories = False
for root, dirs, files in walkLevel(os.path.join(path, 'OEBPS', 'Images'), 1):
if root != os.path.join(path, 'OEBPS', 'Images'):
if len(dirs) != 0:
detectedSubSubdirectories = True
elif len(dirs) == 0 and detectedSubSubdirectories:
print('\nWARNING: Automatic output splitting failed.')
if GUI:
GUI.addMessage.emit('Automatic output splitting failed. <a href='
'"https://github.com/ciromattia/kcc/wiki'
'/Automatic-output-splitting">'
'More details.</a>', 'warning', False)
GUI.addMessage.emit('', '', False)
return [path]
if len(files) != 0:
detectedFilesInSubdirectories = True
if detectedSubSubdirectories:
# Two levels of subdirectories
mode = 2
else:
# One level of subdirectories
mode = 1
if detectedFilesInSubdirectories and detectedSubSubdirectories:
print('\nWARNING: Automatic output splitting failed.')
if GUI:
GUI.addMessage.emit('Automatic output splitting failed. <a href='
'"https://github.com/ciromattia/kcc/wiki'
'/Automatic-output-splitting">'
'More details.</a>', 'warning', False)
GUI.addMessage.emit('', '', False)
return [path]
# Split directories
splitter = splitProcess(os.path.join(path, 'OEBPS', 'Images'), mode)
path = [path]
for tome in splitter:
path.append(tome)
return path
def createNewTome(): def splitProcess(path, mode):
tomePathRoot = mkdtemp('', 'KCC-TMP-')
tomePath = os.path.join(tomePathRoot, 'OEBPS', 'Images')
os.makedirs(tomePath)
return tomePath, tomePathRoot
def splitDirectory(path, mode):
output = [] output = []
currentSize = 0 currentSize = 0
currentTarget = path currentTarget = path
@@ -783,65 +869,6 @@ def splitDirectory(path, mode):
return output return output
#noinspection PyUnboundLocalVariable
def preSplitDirectory(path):
# Detect directory stucture
for root, dirs, files in walkLevel(os.path.join(path, 'OEBPS', 'Images'), 0):
subdirectoryNumber = len(dirs)
filesNumber = len(files)
if subdirectoryNumber == 0:
# No subdirectories
mode = 0
else:
if filesNumber > 0:
print('\nWARNING: Automatic output splitting failed.')
if GUI:
GUI.addMessage.emit('Automatic output splitting failed. <a href='
'"https://github.com/ciromattia/kcc/wiki'
'/Automatic-output-splitting">'
'More details.</a>', 'warning', False)
GUI.addMessage.emit('', '', False)
return [path]
detectedSubSubdirectories = False
detectedFilesInSubdirectories = False
for root, dirs, files in walkLevel(os.path.join(path, 'OEBPS', 'Images'), 1):
if root != os.path.join(path, 'OEBPS', 'Images'):
if len(dirs) != 0:
detectedSubSubdirectories = True
elif len(dirs) == 0 and detectedSubSubdirectories:
print('\nWARNING: Automatic output splitting failed.')
if GUI:
GUI.addMessage.emit('Automatic output splitting failed. <a href='
'"https://github.com/ciromattia/kcc/wiki'
'/Automatic-output-splitting">'
'More details.</a>', 'warning', False)
GUI.addMessage.emit('', '', False)
return [path]
if len(files) != 0:
detectedFilesInSubdirectories = True
if detectedSubSubdirectories:
# Two levels of subdirectories
mode = 2
else:
# One level of subdirectories
mode = 1
if detectedFilesInSubdirectories and detectedSubSubdirectories:
print('\nWARNING: Automatic output splitting failed.')
if GUI:
GUI.addMessage.emit('Automatic output splitting failed. <a href='
'"https://github.com/ciromattia/kcc/wiki'
'/Automatic-output-splitting">'
'More details.</a>', 'warning', False)
GUI.addMessage.emit('', '', False)
return [path]
# Split directories
splitter = splitDirectory(os.path.join(path, 'OEBPS', 'Images'), mode)
path = [path]
for tome in splitter:
path.append(tome)
return path
def detectCorruption(tmpPath, orgPath): def detectCorruption(tmpPath, orgPath):
for root, dirs, files in os.walk(tmpPath, False): for root, dirs, files in os.walk(tmpPath, False):
for name in files: for name in files:
@@ -859,6 +886,50 @@ def detectCorruption(tmpPath, orgPath):
except Exception: except Exception:
rmtree(os.path.join(tmpPath, '..', '..'), True) rmtree(os.path.join(tmpPath, '..', '..'), True)
raise RuntimeError('Image file %s is corrupted.' % pathOrg) raise RuntimeError('Image file %s is corrupted.' % pathOrg)
else:
os.remove(os.path.join(root, name))
def detectMargins(path):
if options.imgproc:
for flag in options.imgIndex[path]:
if "Margins-" in flag:
flag = flag.split('-')
xl = flag[1]
yu = flag[2]
xr = flag[3]
yd = flag[4]
if xl != "0":
xl = "-" + str(float(xl)/100) + "%"
else:
xl = "0%"
if xr != "0":
xr = "-" + str(float(xr)/100) + "%"
else:
xr = "0%"
if yu != "0":
yu = "-" + str(float(yu)/100) + "%"
else:
yu = "0%"
if yd != "0":
yd = "-" + str(float(yd)/100) + "%"
else:
yd = "0%"
return xl, yu, xr, yd
return '0%', '0%', '0%', '0%'
def createNewTome():
tomePathRoot = mkdtemp('', 'KCC-TMP-')
tomePath = os.path.join(tomePathRoot, 'OEBPS', 'Images')
os.makedirs(tomePath)
return tomePath, tomePathRoot
def slugify(value):
value = slugifyExt(value)
value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value))
return value
def makeZIP(zipFilename, baseDir, isEPUB=False): def makeZIP(zipFilename, baseDir, isEPUB=False):
@@ -876,15 +947,6 @@ def makeZIP(zipFilename, baseDir, isEPUB=False):
return zipFilename return zipFilename
def Copyright():
print(('comic2ebook v%(__version__)s. Written by Ciro Mattia Gonano and Pawel Jastrzebski.' % globals()))
def Usage():
print("Generates EPUB/CBZ comic ebook from a bunch of images.")
parser.print_help()
def makeParser(): def makeParser():
"""Create and return an option parser set up with kcc's options.""" """Create and return an option parser set up with kcc's options."""
psr = OptionParser(usage="Usage: kcc-c2e [options] comic_file|comic_folder", add_help_option=False) psr = OptionParser(usage="Usage: kcc-c2e [options] comic_file|comic_folder", add_help_option=False)
@@ -953,124 +1015,6 @@ def makeParser():
return psr return psr
def main(argv=None, qtGUI=None):
global parser, options, GUI
parser = makeParser()
options, args = parser.parse_args(argv)
checkOptions()
if qtGUI:
GUI = qtGUI
GUI.progressBarTick.emit('1')
else:
GUI = None
if len(args) != 1:
parser.print_help()
return
outputPath = makeBook(args[0], qtGUI=qtGUI)
return outputPath
def makeBook(source, qtGUI=None):
"""Generates EPUB/CBZ comic ebook from a bunch of images."""
global GUI
GUI = qtGUI
path = getWorkFolder(source)
print("\nChecking images...")
detectCorruption(os.path.join(path, "OEBPS", "Images"), source)
checkComicInfo(os.path.join(path, "OEBPS", "Images"), source)
if options.webtoon:
if options.customheight > 0:
comic2panel.main(['-y ' + str(options.customheight), '-i', '-m', path], qtGUI)
else:
comic2panel.main(['-y ' + str(image.ProfileData.Profiles[options.profile][1][1]), '-i', '-m', path], qtGUI)
if options.imgproc:
print("\nProcessing images...")
if GUI:
GUI.progressBarTick.emit('Processing images')
dirImgProcess(os.path.join(path, "OEBPS", "Images"))
if GUI:
GUI.progressBarTick.emit('1')
chapterNames = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
if options.batchsplit:
tomes = preSplitDirectory(path)
else:
tomes = [path]
filepath = []
tomeNumber = 0
if GUI:
if options.cbzoutput:
GUI.progressBarTick.emit('Compressing CBZ files')
else:
GUI.progressBarTick.emit('Compressing EPUB files')
GUI.progressBarTick.emit(str(len(tomes) + 1))
GUI.progressBarTick.emit('tick')
options.baseTitle = options.title
for tome in tomes:
if len(tomes) > 1:
tomeNumber += 1
options.title = options.baseTitle + ' [' + str(tomeNumber) + '/' + str(len(tomes)) + ']'
if options.cbzoutput:
# if CBZ output wanted, compress all images and return filepath
print("\nCreating CBZ file...")
if len(tomes) > 1:
filepath.append(getOutputFilename(source, options.output, '.cbz', ' ' + str(tomeNumber)))
else:
filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"))
else:
print("\nCreating EPUB structure...")
genEpubStruct(tome, chapterNames)
# actually zip the ePub
if len(tomes) > 1:
filepath.append(getOutputFilename(source, options.output, '.epub', ' ' + str(tomeNumber)))
else:
filepath.append(getOutputFilename(source, options.output, '.epub', ''))
makeZIP(tome + '_comic', tome, True)
move(tome + '_comic.zip', filepath[-1])
rmtree(tome, True)
if GUI:
GUI.progressBarTick.emit('tick')
return filepath
def getOutputFilename(srcpath, wantedname, ext, tomeNumber):
if srcpath[-1] == os.path.sep:
srcpath = srcpath[:-1]
if not ext.startswith('.'):
ext = '.' + ext
if wantedname is not None:
if wantedname.endswith(ext):
filename = os.path.abspath(wantedname)
elif os.path.isdir(srcpath):
filename = os.path.join(os.path.abspath(options.output), os.path.basename(srcpath) + ext)
else:
filename = os.path.join(os.path.abspath(options.output),
os.path.basename(os.path.splitext(srcpath)[0]) + ext)
elif os.path.isdir(srcpath):
filename = srcpath + tomeNumber + ext
else:
filename = os.path.splitext(srcpath)[0] + tomeNumber + ext
if os.path.isfile(filename):
counter = 0
basename = os.path.splitext(filename)[0]
while os.path.isfile(basename + '_kcc' + str(counter) + ext):
counter += 1
filename = basename + '_kcc' + str(counter) + ext
return filename
def checkOptions(): def checkOptions():
global options global options
options.panelview = True options.panelview = True
@@ -1110,6 +1054,9 @@ def checkOptions():
# CBZ files on Kindle DX/DXG support higher resolution # CBZ files on Kindle DX/DXG support higher resolution
if options.profile == 'KDX' and options.cbzoutput: if options.profile == 'KDX' and options.cbzoutput:
options.customheight = 1200 options.customheight = 1200
# Ultra mode don't work with CBZ format
if options.quality == 2 and options.cbzoutput:
options.quality = 1
# Override profile data # Override profile data
if options.customwidth != 0 or options.customheight != 0: if options.customwidth != 0 or options.customheight != 0:
X = image.ProfileData.Profiles[options.profile][1][0] X = image.ProfileData.Profiles[options.profile][1][0]
@@ -1118,8 +1065,75 @@ def checkOptions():
X = options.customwidth X = options.customwidth
if options.customheight != 0: if options.customheight != 0:
Y = options.customheight Y = options.customheight
newProfile = ("Custom", (X, Y), image.ProfileData.Palette16, image.ProfileData.Profiles[options.profile][3], newProfile = ("Custom", (int(X), int(Y)), image.ProfileData.Palette16,
(int(X*1.5), 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]
def makeBook(source, qtGUI=None):
"""Generates EPUB/CBZ comic ebook from a bunch of images."""
global GUI
GUI = qtGUI
if GUI:
GUI.progressBarTick.emit('1')
path = getWorkFolder(source)
print("\nChecking images...")
getComicInfo(os.path.join(path, "OEBPS", "Images"), source)
detectCorruption(os.path.join(path, "OEBPS", "Images"), source)
if options.webtoon:
if options.customheight > 0:
comic2panel.main(['-y ' + str(options.customheight), '-i', '-m', path], qtGUI)
else:
comic2panel.main(['-y ' + str(image.ProfileData.Profiles[options.profile][1][1]), '-i', '-m', path], qtGUI)
if options.imgproc:
print("\nProcessing images...")
if GUI:
GUI.progressBarTick.emit('Processing images')
imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images"))
if GUI:
GUI.progressBarTick.emit('1')
chapterNames = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
if 'Ko' in options.profile and options.cbzoutput:
sanitizeTreeKobo(os.path.join(path, 'OEBPS', 'Images'))
if options.batchsplit:
tomes = splitDirectory(path)
else:
tomes = [path]
filepath = []
tomeNumber = 0
if GUI:
if options.cbzoutput:
GUI.progressBarTick.emit('Compressing CBZ files')
else:
GUI.progressBarTick.emit('Compressing EPUB files')
GUI.progressBarTick.emit(str(len(tomes) + 1))
GUI.progressBarTick.emit('tick')
options.baseTitle = options.title
for tome in tomes:
if len(tomes) > 1:
tomeNumber += 1
options.title = options.baseTitle + ' [' + str(tomeNumber) + '/' + str(len(tomes)) + ']'
if options.cbzoutput:
# if CBZ output wanted, compress all images and return filepath
print("\nCreating CBZ file...")
if len(tomes) > 1:
filepath.append(getOutputFilename(source, options.output, '.cbz', ' ' + str(tomeNumber)))
else:
filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"))
else:
print("\nCreating EPUB structure...")
buildEPUB(tome, chapterNames, tomeNumber)
# actually zip the ePub
if len(tomes) > 1:
filepath.append(getOutputFilename(source, options.output, '.epub', ' ' + str(tomeNumber)))
else:
filepath.append(getOutputFilename(source, options.output, '.epub', ''))
makeZIP(tome + '_comic', tome, True)
move(tome + '_comic.zip', filepath[-1])
rmtree(tome, True)
if GUI:
GUI.progressBarTick.emit('tick')
return filepath

View File

@@ -18,7 +18,7 @@
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
# #
__version__ = '4.1' __version__ = '4.2'
__license__ = 'ISC' __license__ = 'ISC'
__copyright__ = '2012-2014, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>' __copyright__ = '2012-2014, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
@@ -36,7 +36,7 @@ except ImportError:
QtCore = None QtCore = None
def mergeDirectory_tick(output): def mergeDirectoryTick(output):
if output: if output:
mergeWorkerOutput.append(output) mergeWorkerOutput.append(output)
mergeWorkerPool.terminate() mergeWorkerPool.terminate()
@@ -108,7 +108,7 @@ def sanitizePanelSize(panel, opt):
return newPanels return newPanels
def splitImage_tick(output): def splitImageTick(output):
if output: if output:
splitWorkerOutput.append(output) splitWorkerOutput.append(output)
splitWorkerPool.terminate() splitWorkerPool.terminate()
@@ -207,10 +207,6 @@ def splitImage(work):
return str(sys.exc_info()[1]) return str(sys.exc_info()[1])
def Copyright():
print(('comic2panel v%(__version__)s. Written by Ciro Mattia Gonano and Pawel Jastrzebski.' % globals()))
def main(argv=None, qtGUI=None): def main(argv=None, qtGUI=None):
global options, GUI, splitWorkerPool, splitWorkerOutput, mergeWorkerPool, mergeWorkerOutput global options, GUI, splitWorkerPool, splitWorkerOutput, mergeWorkerPool, mergeWorkerOutput
parser = OptionParser(usage="Usage: kcc-c2p [options] comic_folder", add_help_option=False) parser = OptionParser(usage="Usage: kcc-c2p [options] comic_folder", add_help_option=False)
@@ -261,7 +257,7 @@ def main(argv=None, qtGUI=None):
GUI.progressBarTick.emit('Combining images') GUI.progressBarTick.emit('Combining images')
GUI.progressBarTick.emit(str(directoryNumer)) GUI.progressBarTick.emit(str(directoryNumer))
for i in mergeWork: for i in mergeWork:
mergeWorkerPool.apply_async(func=mergeDirectory, args=(i, ), callback=mergeDirectory_tick) mergeWorkerPool.apply_async(func=mergeDirectory, args=(i, ), callback=mergeDirectoryTick)
mergeWorkerPool.close() mergeWorkerPool.close()
mergeWorkerPool.join() mergeWorkerPool.join()
if GUI and not GUI.conversionAlive: if GUI and not GUI.conversionAlive:
@@ -284,7 +280,7 @@ def main(argv=None, qtGUI=None):
GUI.progressBarTick.emit('tick') GUI.progressBarTick.emit('tick')
if len(work) > 0: if len(work) > 0:
for i in work: for i in work:
splitWorkerPool.apply_async(func=splitImage, args=(i, ), callback=splitImage_tick) splitWorkerPool.apply_async(func=splitImage, args=(i, ), callback=splitImageTick)
splitWorkerPool.close() splitWorkerPool.close()
splitWorkerPool.join() splitWorkerPool.join()
if GUI and not GUI.conversionAlive: if GUI and not GUI.conversionAlive:

View File

@@ -16,11 +16,14 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
__version__ = '4.2'
__license__ = 'ISC' __license__ = 'ISC'
__copyright__ = '2012-2014, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>' __copyright__ = '2012-2014, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os import os
from io import BytesIO
from urllib.request import Request, urlopen
from functools import reduce from functools import reduce
from PIL import Image, ImageOps, ImageStat, ImageChops from PIL import Image, ImageOps, ImageStat, ImageChops
from .shared import md5Checksum from .shared import md5Checksum
@@ -472,14 +475,37 @@ class ComicPage:
class Cover: class Cover:
def __init__(self, source, target): def __init__(self, source, target, opt, tomeNumber):
self.options = opt
self.source = source self.source = source
self.target = target self.target = target
if tomeNumber == 0:
self.tomeNumber = 1
else:
self.tomeNumber = tomeNumber
if self.tomeNumber in self.options.remoteCovers:
try:
source = urlopen(Request(self.options.remoteCovers[self.tomeNumber],
headers={'User-Agent': 'KindleComicConverter/' + __version__})).read()
self.image = Image.open(BytesIO(source))
self.processExternal()
except Exception:
self.image = Image.open(source) self.image = Image.open(source)
self.processInternal()
else:
self.image = Image.open(source)
self.processInternal()
def processInternal(self):
self.image = self.image.convert('RGB') self.image = self.image.convert('RGB')
self.process() self.image = self.trim()
self.save() self.save()
def processExternal(self):
self.image = self.image.convert('RGB')
self.image.thumbnail(self.options.profileData[1], Image.ANTIALIAS)
self.save(True)
def trim(self): def trim(self):
bg = Image.new(self.image.mode, self.image.size, self.image.getpixel((0, 0))) bg = Image.new(self.image.mode, self.image.size, self.image.getpixel((0, 0)))
diff = ImageChops.difference(self.image, bg) diff = ImageChops.difference(self.image, bg)
@@ -490,12 +516,13 @@ class Cover:
else: else:
return self.image return self.image
def process(self): def save(self, external=False):
self.image = self.trim() if external:
source = self.options.remoteCovers[self.tomeNumber].split('/')[-1]
def save(self): else:
source = self.source
try: try:
if os.path.splitext(self.source)[1].lower() == '.png': if os.path.splitext(source)[1].lower() == '.png':
self.image.save(self.target, "PNG", optimize=1) self.image.save(self.target, "PNG", optimize=1)
else: else:
self.image.save(self.target, "JPEG", optimize=1) self.image.save(self.target, "JPEG", optimize=1)

View File

@@ -14,7 +14,7 @@ if version_info[0] != 3:
exit(1) exit(1)
NAME = "KindleComicConverter" NAME = "KindleComicConverter"
VERSION = "4.1" VERSION = "4.2"
MAIN = "kcc.py" MAIN = "kcc.py"
if platform == "darwin": if platform == "darwin":

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# Linux Python package build script # Linux Python package build script
VERSION="4.1" VERSION="4.2"
cp kcc.py __main__.py cp kcc.py __main__.py
zip kcc.zip __main__.py kcc/*.py zip kcc.zip __main__.py kcc/*.py