mirror of
https://github.com/ciromattia/kcc
synced 2025-12-13 01:36:27 +00:00
10
README.md
10
README.md
@@ -1,6 +1,6 @@
|
||||
# KCC
|
||||
|
||||
[]() []() []()
|
||||
[](https://github.com/ciromattia/kcc/releases) [](https://pypi.python.org/pypi/KindleComicConverter) [](https://aur.archlinux.org/packages/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.
|
||||
It was initially developed for Kindle but since version 4.6 it outputs valid EPUB 3.0 so _**despite its name, KCC is
|
||||
@@ -72,7 +72,7 @@ After completed conversion you should find ready file alongside the original inp
|
||||
|
||||
Please check [our wiki](https://github.com/ciromattia/kcc/wiki/) for more details.
|
||||
|
||||
CLI version of **KCC** is intended for power users. It is not idiot-proof like GUI :-)
|
||||
CLI version of **KCC** is intended for power users. It allow to use options that might not be compatible and decrease quality of output.
|
||||
|
||||
### Standalone `kcc-c2e.py` usage:
|
||||
|
||||
@@ -86,6 +86,7 @@ Options:
|
||||
KDX, KPW, KV, KoMT, KoG, KoGHD, KoA, KoAHD, KoAH2O,
|
||||
KoAO) [Default=KV]
|
||||
-m, --manga-style Manga style (right-to-left reading and splitting)
|
||||
-q, --hq Try to increase the quality of magnification
|
||||
-2, --two-panel Display two not four panels in Panel View mode
|
||||
-w, --webtoon Webtoon processing mode
|
||||
|
||||
@@ -170,6 +171,11 @@ The app relies and includes the following scripts:
|
||||
* [Kobo Aura ONE](http://kcc.iosphe.re/Samples/Ubunchu-KoAO.kepub.epub)
|
||||
|
||||
## CHANGELOG
|
||||
#### 5.4:
|
||||
* Reimplemented high quality Panel View option
|
||||
* Improved webtoon splitter
|
||||
* Fixed page splitter
|
||||
|
||||
#### 5.3.1:
|
||||
* Small increase of output quality
|
||||
* Improved error reporting
|
||||
|
||||
62
gui/KCC.ui
62
gui/KCC.ui
@@ -66,7 +66,16 @@
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="0" column="2">
|
||||
@@ -127,7 +136,16 @@
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QWidget" name="optionWidget" native="true">
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
@@ -156,10 +174,13 @@
|
||||
<item row="0" column="2">
|
||||
<widget class="QCheckBox" name="qualityBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p style=\'white-space:pre\'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 4 panels<br/></span>Zoom each corner separately.</p><p style=\'white-space:pre\'><span style=\" font-weight:600; text-decoration: underline;\">Checked - 2 panels<br/></span>Zoom only the top and bottom of the page.</p></body></html></string>
|
||||
<string><html><head/><body><p style='white-space:pre'><span style=" font-weight:600; text-decoration: underline;">Unchecked - 4 panels<br/></span>Zoom each corner separately.</p><p style='white-space:pre'><span style=" font-weight:600; text-decoration: underline;">Indeterminate - 2 panels<br/></span>Zoom only the top and bottom of the page.</p><p><span style=" font-weight:600; text-decoration: underline;">Checked - 4 high quality panels<br/></span>Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Panel View 4/2</string>
|
||||
<string>Panel View 4/2/HQ</string>
|
||||
</property>
|
||||
<property name="tristate">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -238,7 +259,16 @@
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
@@ -267,7 +297,16 @@
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QWidget" name="toolWidget" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
@@ -316,7 +355,16 @@
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<property name="margin">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
|
||||
@@ -28,4 +28,4 @@ from kindlecomicconverter.startup import startC2P
|
||||
|
||||
if __name__ == "__main__":
|
||||
freeze_support()
|
||||
startC2P()
|
||||
startC2P()
|
||||
|
||||
2
kcc.iss
2
kcc.iss
@@ -1,5 +1,5 @@
|
||||
#define MyAppName "Kindle Comic Converter"
|
||||
#define MyAppVersion "5.3.1"
|
||||
#define MyAppVersion "5.4"
|
||||
#define MyAppPublisher "Ciro Mattia Gonano, Paweł Jastrzębski"
|
||||
#define MyAppURL "http://kcc.iosphe.re/"
|
||||
#define MyAppExeName "KCC.exe"
|
||||
|
||||
2
kcc.py
2
kcc.py
@@ -39,7 +39,7 @@ elif sys.platform.startswith('win'):
|
||||
class _Popen(forking.Popen):
|
||||
def __init__(self, *args, **kw):
|
||||
if hasattr(sys, 'frozen'):
|
||||
# noinspection PyProtectedMember
|
||||
# noinspection PyUnresolvedReferences,PyProtectedMember
|
||||
os.putenv('_MEIPASS2', sys._MEIPASS)
|
||||
try:
|
||||
super(_Popen, self).__init__(*args, **kw)
|
||||
|
||||
@@ -254,8 +254,10 @@ class WorkerThread(QtCore.QThread):
|
||||
options.splitter = 2
|
||||
elif GUI.rotateBox.checkState() == 2:
|
||||
options.splitter = 1
|
||||
if GUI.qualityBox.isChecked():
|
||||
if GUI.qualityBox.checkState() == 1:
|
||||
options.autoscale = True
|
||||
elif GUI.qualityBox.checkState() == 2:
|
||||
options.hq = True
|
||||
if GUI.webtoonBox.isChecked():
|
||||
options.webtoon = True
|
||||
if GUI.upscaleBox.checkState() == 1:
|
||||
@@ -531,8 +533,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
def clearJobs(self):
|
||||
GUI.jobList.clear()
|
||||
|
||||
# noinspection PyCallByClass,PyTypeChecker
|
||||
def openWiki(self):
|
||||
# noinspection PyCallByClass
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl('https://github.com/ciromattia/kcc/wiki'))
|
||||
|
||||
def modeChange(self, mode):
|
||||
@@ -613,6 +615,18 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
GUI.rotateBox.setEnabled(True)
|
||||
GUI.upscaleBox.setEnabled(True)
|
||||
|
||||
def togglequalityBox(self, value):
|
||||
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
|
||||
if value == 2:
|
||||
if profile['Label'] in ['KV']:
|
||||
self.addMessage('This option is intended for older Kindle models.', 'warning')
|
||||
self.addMessage('It might not provide any quality increase in this case.', 'warning')
|
||||
GUI.upscaleBox.setEnabled(False)
|
||||
GUI.upscaleBox.setChecked(True)
|
||||
else:
|
||||
GUI.upscaleBox.setEnabled(True)
|
||||
GUI.upscaleBox.setChecked(profile['DefaultUpscale'])
|
||||
|
||||
def changeGamma(self, value):
|
||||
valueRaw = int(5 * round(float(value) / 5))
|
||||
value = '%.2f' % (float(valueRaw) / 100)
|
||||
@@ -634,7 +648,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
self.changeFormat()
|
||||
GUI.gammaSlider.setValue(0)
|
||||
self.changeGamma(0)
|
||||
GUI.qualityBox.setEnabled(profile['PVOptions'])
|
||||
if not GUI.webtoonBox.isChecked():
|
||||
GUI.qualityBox.setEnabled(profile['PVOptions'])
|
||||
GUI.upscaleBox.setChecked(profile['DefaultUpscale'])
|
||||
if not profile['PVOptions']:
|
||||
GUI.qualityBox.setChecked(False)
|
||||
@@ -648,7 +663,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
GUI.formatBox.setCurrentIndex(outputFormat)
|
||||
else:
|
||||
GUI.formatBox.setCurrentIndex(profile['DefaultFormat'])
|
||||
GUI.qualityBox.setEnabled(profile['PVOptions'])
|
||||
if not GUI.webtoonBox.isChecked():
|
||||
GUI.qualityBox.setEnabled(profile['PVOptions'])
|
||||
if str(GUI.formatBox.currentText()) == 'MOBI/AZW3':
|
||||
GUI.outputSplit.setEnabled(True)
|
||||
else:
|
||||
@@ -1002,6 +1018,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
GUI.gammaSlider.valueChanged.connect(self.changeGamma)
|
||||
GUI.gammaBox.stateChanged.connect(self.togglegammaBox)
|
||||
GUI.webtoonBox.stateChanged.connect(self.togglewebtoonBox)
|
||||
GUI.qualityBox.stateChanged.connect(self.togglequalityBox)
|
||||
GUI.deviceBox.activated.connect(self.changeDevice)
|
||||
GUI.formatBox.activated.connect(self.changeFormat)
|
||||
MW.progressBarTick.connect(self.updateProgressbar)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file 'gui\KCC.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.6
|
||||
# Created by: PyQt5 UI code generator 5.8.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@@ -81,6 +81,7 @@ class Ui_mainWindow(object):
|
||||
self.rotateBox.setObjectName("rotateBox")
|
||||
self.gridLayout_2.addWidget(self.rotateBox, 0, 1, 1, 1)
|
||||
self.qualityBox = QtWidgets.QCheckBox(self.optionWidget)
|
||||
self.qualityBox.setTristate(True)
|
||||
self.qualityBox.setObjectName("qualityBox")
|
||||
self.gridLayout_2.addWidget(self.qualityBox, 0, 2, 1, 1)
|
||||
self.webtoonBox = QtWidgets.QCheckBox(self.optionWidget)
|
||||
@@ -241,8 +242,8 @@ class Ui_mainWindow(object):
|
||||
self.mangaBox.setText(_translate("mainWindow", "Manga mode"))
|
||||
self.rotateBox.setToolTip(_translate("mainWindow", "<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Split<br/></span>Double page spreads will be cut into two separate pages.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Rotate and split<br/></span>Double page spreads will be displayed twice. First rotated and then split. </p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Rotate<br/></span>Double page spreads will be rotated.</p></body></html>"))
|
||||
self.rotateBox.setText(_translate("mainWindow", "Spread splitter"))
|
||||
self.qualityBox.setToolTip(_translate("mainWindow", "<html><head/><body><p style=\'white-space:pre\'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 4 panels<br/></span>Zoom each corner separately.</p><p style=\'white-space:pre\'><span style=\" font-weight:600; text-decoration: underline;\">Checked - 2 panels<br/></span>Zoom only the top and bottom of the page.</p></body></html>"))
|
||||
self.qualityBox.setText(_translate("mainWindow", "Panel View 4/2"))
|
||||
self.qualityBox.setToolTip(_translate("mainWindow", "<html><head/><body><p style=\'white-space:pre\'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 4 panels<br/></span>Zoom each corner separately.</p><p style=\'white-space:pre\'><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - 2 panels<br/></span>Zoom only the top and bottom of the page.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 4 high quality panels<br/></span>Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.</p></body></html>"))
|
||||
self.qualityBox.setText(_translate("mainWindow", "Panel View 4/2/HQ"))
|
||||
self.webtoonBox.setToolTip(_translate("mainWindow", "<html><head/><body><p style=\'white-space:pre\'>Enable special parsing mode for Korean Webtoons.</p></body></html>"))
|
||||
self.webtoonBox.setText(_translate("mainWindow", "Webtoon mode"))
|
||||
self.upscaleBox.setToolTip(_translate("mainWindow", "<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Nothing<br/></span>Images smaller than device resolution will not be resized.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - Stretching<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be not preserved.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Upscaling<br/></span>Images smaller than device resolution will be resized. Aspect ratio will be preserved.</p></body></html>"))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
__version__ = '5.3.1'
|
||||
__version__ = '5.4'
|
||||
__license__ = 'ISC'
|
||||
__copyright__ = '2012-2017, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
@@ -16,12 +16,11 @@
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
#
|
||||
|
||||
import sys
|
||||
import os
|
||||
from zipfile import is_zipfile, ZipFile
|
||||
from subprocess import STDOUT, PIPE
|
||||
from psutil import Popen
|
||||
from shutil import move, copy
|
||||
from shutil import move
|
||||
from . import rarfile
|
||||
from .shared import check7ZFile as is_7zfile
|
||||
|
||||
|
||||
@@ -103,6 +103,10 @@ def buildHTML(path, imgfile, imgfilepath):
|
||||
os.makedirs(htmlpath)
|
||||
htmlfile = os.path.join(htmlpath, filename[0] + '.xhtml')
|
||||
imgsize = Image.open(os.path.join(head, "Images", postfix, imgfile)).size
|
||||
if options.hq:
|
||||
imgsizeframe = deviceres
|
||||
else:
|
||||
imgsizeframe = imgsize
|
||||
f = open(htmlfile, "w", encoding='UTF-8')
|
||||
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
|
||||
"<!DOCTYPE html>\n",
|
||||
@@ -111,17 +115,20 @@ def buildHTML(path, imgfile, imgfilepath):
|
||||
"<title>", escape(filename[0]), "</title>\n",
|
||||
"<link href=\"", "../" * (backref - 1), "style.css\" type=\"text/css\" rel=\"stylesheet\"/>\n",
|
||||
"<meta name=\"viewport\" "
|
||||
"content=\"width=" + str(deviceres[0]) + ", height=" + str(deviceres[1]) + "\"/>\n"
|
||||
"content=\"width=" + str(imgsize[0]) + ", height=" + str(imgsize[1]) + "\"/>\n"
|
||||
"</head>\n",
|
||||
"<body style=\"" + additionalStyle + "\">\n",
|
||||
"<div style=\"text-align:center;top:" + getTopMargin(deviceres, imgsize) + "%;\">\n",
|
||||
"<img width=\"" + str(imgsize[0]) + "\" height=\"" + str(imgsize[1]) + "\" ",
|
||||
"<img width=\"" + str(imgsizeframe[0]) + "\" height=\"" + str(imgsizeframe[1]) + "\" ",
|
||||
"src=\"", "../" * backref, "Images/", postfix, imgfile, "\"/>\n</div>\n"])
|
||||
if options.iskindle and options.panelview:
|
||||
if options.autoscale:
|
||||
size = (getPanelViewResolution(imgsize, deviceres))
|
||||
else:
|
||||
size = (int(imgsize[0] * 1.5), int(imgsize[1] * 1.5))
|
||||
if options.hq:
|
||||
size = imgsize
|
||||
else:
|
||||
size = (int(imgsize[0] * 1.5), int(imgsize[1] * 1.5))
|
||||
if size[0] - deviceres[0] < deviceres[0] * 0.01:
|
||||
noHorizontalPV = True
|
||||
else:
|
||||
@@ -455,7 +462,7 @@ def buildEPUB(path, chapterNames, tomeNumber):
|
||||
|
||||
def imgDirectoryProcessing(path):
|
||||
global workerPool, workerOutput
|
||||
workerPool = Pool()
|
||||
workerPool = Pool(maxtasksperchild=100)
|
||||
workerOutput = []
|
||||
options.imgMetadata = {}
|
||||
options.imgOld = []
|
||||
@@ -884,6 +891,8 @@ def makeParser():
|
||||
" KoA, KoAHD, KoAH2O, KoAO) [Default=KV]")
|
||||
mainOptions.add_option("-m", "--manga-style", action="store_true", dest="righttoleft", default=False,
|
||||
help="Manga style (right-to-left reading and splitting)")
|
||||
mainOptions.add_option("-q", "--hq", action="store_true", dest="hq", default=False,
|
||||
help="Try to increase the quality of magnification")
|
||||
mainOptions.add_option("-2", "--two-panel", action="store_true", dest="autoscale", default=False,
|
||||
help="Display two not four panels in Panel View mode")
|
||||
mainOptions.add_option("-w", "--webtoon", action="store_true", dest="webtoon", default=False,
|
||||
@@ -960,16 +969,20 @@ def checkOptions():
|
||||
# Older Kindle models don't support Panel View.
|
||||
if options.profile == 'K1' or options.profile == 'K2' or options.profile == 'KDX':
|
||||
options.panelview = False
|
||||
options.hq = False
|
||||
# Webtoon mode mandatory options
|
||||
if options.webtoon:
|
||||
options.panelview = False
|
||||
options.righttoleft = False
|
||||
options.upscale = True
|
||||
options.hq = False
|
||||
# Disable all Kindle features for other e-readers
|
||||
if options.profile == 'OTHER':
|
||||
options.panelview = False
|
||||
options.hq = False
|
||||
if 'Ko' in options.profile:
|
||||
options.panelview = False
|
||||
options.hq = False
|
||||
# CBZ files on Kindle DX/DXG support higher resolution
|
||||
if options.profile == 'KDX' and options.format == 'CBZ':
|
||||
options.customheight = 1200
|
||||
@@ -1185,9 +1198,11 @@ def makeMOBI(work, qtGUI=None):
|
||||
threadNumber = 1
|
||||
elif 2 < availableMemory <= 4:
|
||||
threadNumber = 2
|
||||
else:
|
||||
elif 4 < availableMemory <= 8:
|
||||
threadNumber = 4
|
||||
makeMOBIWorkerPool = Pool(threadNumber)
|
||||
else:
|
||||
threadNumber = None
|
||||
makeMOBIWorkerPool = Pool(threadNumber, maxtasksperchild=10)
|
||||
for i in work:
|
||||
makeMOBIWorkerPool.apply_async(func=makeMOBIWorker, args=(i, ), callback=makeMOBIWorkerTick)
|
||||
makeMOBIWorkerPool.close()
|
||||
|
||||
@@ -23,7 +23,7 @@ import sys
|
||||
from shutil import rmtree, copytree, move
|
||||
from optparse import OptionParser, OptionGroup
|
||||
from multiprocessing import Pool
|
||||
from PIL import Image, ImageStat, ImageOps
|
||||
from PIL import Image, ImageChops, ImageOps, ImageDraw
|
||||
from .shared import getImageFileName, walkLevel, walkSort, sanitizeTrace
|
||||
try:
|
||||
from PyQt5 import QtCore
|
||||
@@ -67,8 +67,7 @@ def mergeDirectory(work):
|
||||
result = Image.new('RGB', (targetWidth, targetHeight))
|
||||
y = 0
|
||||
for i in imagesValid:
|
||||
img = Image.open(i)
|
||||
img = img.convert('RGB')
|
||||
img = Image.open(i).convert('RGB')
|
||||
if img.size[0] < targetWidth:
|
||||
img = ImageOps.fit(img, (targetWidth, img.size[1]), method=Image.BICUBIC, centering=(0.5, 0.5))
|
||||
result.paste(img, (0, y))
|
||||
@@ -80,30 +79,8 @@ def mergeDirectory(work):
|
||||
return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2])
|
||||
|
||||
|
||||
def sanitizePanelSize(panel, opt):
|
||||
newPanels = []
|
||||
if panel[2] > 6 * opt.height:
|
||||
diff = int(panel[2] / 8)
|
||||
newPanels.append([panel[0], panel[1] - diff * 7, diff])
|
||||
newPanels.append([panel[1] - diff * 7, panel[1] - diff * 6, diff])
|
||||
newPanels.append([panel[1] - diff * 6, panel[1] - diff * 5, diff])
|
||||
newPanels.append([panel[1] - diff * 5, panel[1] - diff * 4, diff])
|
||||
newPanels.append([panel[1] - diff * 4, panel[1] - diff * 3, diff])
|
||||
newPanels.append([panel[1] - diff * 3, panel[1] - diff * 2, diff])
|
||||
newPanels.append([panel[1] - diff * 2, panel[1] - diff, diff])
|
||||
newPanels.append([panel[1] - diff, panel[1], diff])
|
||||
elif panel[2] > 3 * opt.height:
|
||||
diff = int(panel[2] / 4)
|
||||
newPanels.append([panel[0], panel[1] - diff * 3, diff])
|
||||
newPanels.append([panel[1] - diff * 3, panel[1] - diff * 2, diff])
|
||||
newPanels.append([panel[1] - diff * 2, panel[1] - diff, diff])
|
||||
newPanels.append([panel[1] - diff, panel[1], diff])
|
||||
elif panel[2] > 1.5 * opt.height:
|
||||
newPanels.append([panel[0], panel[1] - int(panel[2] / 2), int(panel[2] / 2)])
|
||||
newPanels.append([panel[1] - int(panel[2] / 2), panel[1], int(panel[2] / 2)])
|
||||
else:
|
||||
newPanels = [panel]
|
||||
return newPanels
|
||||
def detectSolid(img):
|
||||
return not ImageChops.invert(img).getbbox() or not img.getbbox()
|
||||
|
||||
|
||||
def splitImageTick(output):
|
||||
@@ -121,56 +98,60 @@ def splitImage(work):
|
||||
path = work[0]
|
||||
name = work[1]
|
||||
opt = work[2]
|
||||
# Hardcoded options
|
||||
threshold = 1.0
|
||||
delta = 15
|
||||
fileExpanded = os.path.splitext(name)
|
||||
filePath = os.path.join(path, name)
|
||||
image = Image.open(filePath)
|
||||
image = image.convert('RGB')
|
||||
widthImg, heightImg = image.size
|
||||
imgOrg = Image.open(filePath).convert('RGB')
|
||||
imgProcess = Image.open(filePath).convert('1')
|
||||
widthImg, heightImg = imgOrg.size
|
||||
if heightImg > opt.height:
|
||||
if opt.debug:
|
||||
from PIL import ImageDraw
|
||||
debugImage = Image.open(filePath)
|
||||
draw = ImageDraw.Draw(debugImage)
|
||||
drawImg = Image.open(filePath).convert(mode='RGBA')
|
||||
draw = ImageDraw.Draw(drawImg)
|
||||
|
||||
# Find panels
|
||||
y1 = 0
|
||||
y2 = 15
|
||||
yWork = 0
|
||||
panelDetected = False
|
||||
panels = []
|
||||
while y2 < heightImg:
|
||||
while ImageStat.Stat(image.crop([0, y1, widthImg, y2])).var[0] < threshold and y2 < heightImg:
|
||||
y2 += delta
|
||||
y2 -= delta
|
||||
y1Temp = y2
|
||||
y1 = y2 + delta
|
||||
y2 = y1 + delta
|
||||
while ImageStat.Stat(image.crop([0, y1, widthImg, y2])).var[0] >= threshold and y2 < heightImg:
|
||||
y1 += delta
|
||||
y2 += delta
|
||||
if y1 + delta >= heightImg:
|
||||
y1 = heightImg - 1
|
||||
y2Temp = y1
|
||||
if opt.debug:
|
||||
draw.line([(0, y1Temp), (widthImg, y1Temp)], fill=(0, 255, 0))
|
||||
draw.line([(0, y2Temp), (widthImg, y2Temp)], fill=(255, 0, 0))
|
||||
panelHeight = y2Temp - y1Temp
|
||||
if panelHeight > delta:
|
||||
# Panels that can't be cut nicely will be forcefully splitted
|
||||
panelsCleaned = sanitizePanelSize([y1Temp, y2Temp, panelHeight], opt)
|
||||
for panel in panelsCleaned:
|
||||
panels.append(panel)
|
||||
while yWork < heightImg:
|
||||
tmpImg = imgProcess.crop([0, yWork, widthImg, yWork + 4])
|
||||
solid = detectSolid(tmpImg)
|
||||
if not solid and not panelDetected:
|
||||
panelDetected = True
|
||||
panelY1 = yWork - 2
|
||||
if solid and panelDetected:
|
||||
panelDetected = False
|
||||
panelY2 = yWork + 6
|
||||
panels.append((panelY1, panelY2, panelY2 - panelY1))
|
||||
yWork += 5
|
||||
|
||||
# Split too big panels
|
||||
panelsProcessed = []
|
||||
for panel in panels:
|
||||
if panel[2] <= opt.height * 1.5:
|
||||
panelsProcessed.append(panel)
|
||||
elif panel[2] < opt.height * 2:
|
||||
diff = panel[2] - opt.height
|
||||
panelsProcessed.append((panel[0], panel[1] - diff, opt.height))
|
||||
panelsProcessed.append((panel[1] - opt.height, panel[1], opt.height))
|
||||
else:
|
||||
parts = round(panel[2] / opt.height)
|
||||
diff = panel[2] // parts
|
||||
for x in range(0, parts):
|
||||
panelsProcessed.append((panel[0] + (x * diff), panel[1] - ((parts - x - 1) * diff), diff))
|
||||
|
||||
if opt.debug:
|
||||
for panel in panelsProcessed:
|
||||
# noinspection PyUnboundLocalVariable
|
||||
draw.rectangle([(0, panel[0]), (widthImg, panel[1])], (0, 255, 0, 128), (0, 0, 255, 255))
|
||||
# noinspection PyUnboundLocalVariable
|
||||
debugImage.save(os.path.join(path, fileExpanded[0] + '-debug.png'), 'PNG')
|
||||
debugImage = Image.alpha_composite(imgOrg.convert(mode='RGBA'), drawImg)
|
||||
debugImage.save(os.path.join(path, os.path.splitext(name)[0] + '-debug.png'), 'PNG')
|
||||
|
||||
# Create virtual pages
|
||||
pages = []
|
||||
currentPage = []
|
||||
pageLeft = opt.height
|
||||
panelNumber = 0
|
||||
for panel in panels:
|
||||
for panel in panelsProcessed:
|
||||
if pageLeft - panel[2] > 0:
|
||||
pageLeft -= panel[2]
|
||||
currentPage.append(panelNumber)
|
||||
@@ -190,14 +171,14 @@ def splitImage(work):
|
||||
pageHeight = 0
|
||||
targetHeight = 0
|
||||
for panel in page:
|
||||
pageHeight += panels[panel][2]
|
||||
if pageHeight > delta:
|
||||
pageHeight += panelsProcessed[panel][2]
|
||||
if pageHeight > 15:
|
||||
newPage = Image.new('RGB', (widthImg, pageHeight))
|
||||
for panel in page:
|
||||
panelImg = image.crop([0, panels[panel][0], widthImg, panels[panel][1]])
|
||||
panelImg = imgOrg.crop([0, panelsProcessed[panel][0], widthImg, panelsProcessed[panel][1]])
|
||||
newPage.paste(panelImg, (0, targetHeight))
|
||||
targetHeight += panels[panel][2]
|
||||
newPage.save(os.path.join(path, fileExpanded[0] + '-' + str(pageNumber) + '.png'), 'PNG')
|
||||
targetHeight += panelsProcessed[panel][2]
|
||||
newPage.save(os.path.join(path, os.path.splitext(name)[0] + '-' + str(pageNumber) + '.png'), 'PNG')
|
||||
pageNumber += 1
|
||||
os.remove(filePath)
|
||||
except Exception:
|
||||
@@ -238,13 +219,13 @@ def main(argv=None, qtGUI=None):
|
||||
work = []
|
||||
pagenumber = 1
|
||||
splitWorkerOutput = []
|
||||
splitWorkerPool = Pool()
|
||||
splitWorkerPool = Pool(maxtasksperchild=10)
|
||||
if options.merge:
|
||||
print("Merging images...")
|
||||
directoryNumer = 1
|
||||
mergeWork = []
|
||||
mergeWorkerOutput = []
|
||||
mergeWorkerPool = Pool()
|
||||
mergeWorkerPool = Pool(maxtasksperchild=10)
|
||||
mergeWork.append([options.targetDir])
|
||||
for root, dirs, files in os.walk(options.targetDir, False):
|
||||
dirs, files = walkSort(dirs, files)
|
||||
@@ -263,7 +244,8 @@ def main(argv=None, qtGUI=None):
|
||||
raise UserWarning("Conversion interrupted.")
|
||||
if len(mergeWorkerOutput) > 0:
|
||||
rmtree(options.targetDir, True)
|
||||
raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0], mergeWorkerOutput[0][1])
|
||||
raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0],
|
||||
mergeWorkerOutput[0][1])
|
||||
print("Splitting images...")
|
||||
for root, _, files in os.walk(options.targetDir, False):
|
||||
for name in files:
|
||||
@@ -286,7 +268,8 @@ def main(argv=None, qtGUI=None):
|
||||
raise UserWarning("Conversion interrupted.")
|
||||
if len(splitWorkerOutput) > 0:
|
||||
rmtree(options.targetDir, True)
|
||||
raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0], splitWorkerOutput[0][1])
|
||||
raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0],
|
||||
splitWorkerOutput[0][1])
|
||||
if options.inPlace:
|
||||
rmtree(options.sourceDir)
|
||||
move(options.targetDir, options.sourceDir)
|
||||
|
||||
@@ -120,7 +120,7 @@ class ComicPageParser:
|
||||
width, height = self.image.size
|
||||
dstwidth, dstheight = self.size
|
||||
if (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth \
|
||||
and not self.opt.webtoon:
|
||||
and not self.opt.webtoon and self.opt.splitter == 1:
|
||||
self.payload.append(['R', self.source, self.image.rotate(90, Image.BICUBIC, True), self.color, self.fill])
|
||||
elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon:
|
||||
if self.opt.splitter != 1:
|
||||
@@ -209,6 +209,8 @@ class ComicPage:
|
||||
def __init__(self, options, mode, path, image, color, fill):
|
||||
self.opt = options
|
||||
_, self.size, self.palette, self.gamma = self.opt.profileData
|
||||
if self.opt.hq:
|
||||
self.size = (int(self.size[0] * 1.5), int(self.size[1] * 1.5))
|
||||
self.image = image
|
||||
self.color = color
|
||||
self.fill = fill
|
||||
|
||||
@@ -17,11 +17,9 @@
|
||||
#
|
||||
|
||||
import os
|
||||
from sys import version_info
|
||||
from hashlib import md5
|
||||
from html.parser import HTMLParser
|
||||
from distutils.version import StrictVersion
|
||||
from time import sleep
|
||||
from shutil import rmtree, copy
|
||||
from tempfile import mkdtemp
|
||||
from zipfile import ZipFile, ZIP_DEFLATED
|
||||
|
||||
@@ -23,6 +23,7 @@ import sys
|
||||
from . import __version__
|
||||
from .shared import dependencyCheck
|
||||
|
||||
|
||||
def start():
|
||||
dependencyCheck(3)
|
||||
from . import KCC_gui
|
||||
@@ -40,14 +41,16 @@ def start():
|
||||
KCCUI.handleMessage(sys.argv[1])
|
||||
sys.exit(KCCAplication.exec_())
|
||||
|
||||
|
||||
def startC2E():
|
||||
dependencyCheck(2)
|
||||
from .comic2ebook import main
|
||||
print('comic2ebook v' + __version__ + ' - Written by Ciro Mattia Gonano and Pawel Jastrzebski.')
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
|
||||
|
||||
def startC2P():
|
||||
dependencyCheck(1)
|
||||
from .comic2panel import main
|
||||
print('comic2panel v' + __version__ + ' - Written by Ciro Mattia Gonano and Pawel Jastrzebski.')
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>MacOS/Kindle Comic Converter</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>KindleComicConverter 5.3.1, written 2012-2017 by Ciro Mattia Gonano and Pawel Jastrzebski</string>
|
||||
<string>KindleComicConverter 5.4.0, written 2012-2017 by Ciro Mattia Gonano and Pawel Jastrzebski</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>comic2ebook.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -42,11 +42,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>5.3.1</string>
|
||||
<string>5.4.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>5.3.1</string>
|
||||
<string>5.4.0</string>
|
||||
<key>LSEnvironment</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
|
||||
22
setup.py
22
setup.py
@@ -2,10 +2,10 @@
|
||||
"""
|
||||
pip/pyinstaller build script for KCC.
|
||||
|
||||
Usage (Windows):
|
||||
py -3 setup.py build_binary
|
||||
Install as Python package:
|
||||
python3 setup.py install
|
||||
|
||||
Usage (Linux/OS X):
|
||||
Create EXE/APP/DEB:
|
||||
python3 setup.py build_binary
|
||||
"""
|
||||
|
||||
@@ -14,32 +14,22 @@ import sys
|
||||
import shutil
|
||||
import setuptools
|
||||
import distutils.cmd
|
||||
from distutils.command.build import build
|
||||
from kindlecomicconverter import __version__
|
||||
|
||||
NAME = 'KindleComicConverter'
|
||||
MAIN = 'kcc.py'
|
||||
VERSION = __version__
|
||||
|
||||
|
||||
class BuildBinaryCommand(distutils.cmd.Command):
|
||||
description = 'build binary release'
|
||||
user_options = [
|
||||
('pyz', None, 'build PYZ package'),
|
||||
]
|
||||
|
||||
def initialize_options(self):
|
||||
# noinspection PyAttributeOutsideInit
|
||||
self.pyz = False
|
||||
|
||||
def finalize_options(self):
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
if sys.platform == 'darwin':
|
||||
if os.path.isfile('Kindle Comic Converter.spec'):
|
||||
os.system('pyinstaller "Kindle Comic Converter.spec"')
|
||||
else:
|
||||
os.system('pyinstaller -y -F -i icons/comic2ebook.icns -n "Kindle Comic Converter" -w -s --noupx kcc.py')
|
||||
os.system('pyinstaller -y -F -i icons/comic2ebook.icns -n "Kindle Comic Converter" -w -s kcc.py')
|
||||
shutil.copy('other/osx/7za', 'dist/Kindle Comic Converter.app/Contents/Resources')
|
||||
shutil.copy('other/osx/unrar', 'dist/Kindle Comic Converter.app/Contents/Resources')
|
||||
shutil.copy('other/osx/Info.plist', 'dist/Kindle Comic Converter.app/Contents')
|
||||
@@ -92,6 +82,6 @@ setuptools.setup(
|
||||
'python-slugify>=1.2.1',
|
||||
'raven>=6.0.0',
|
||||
],
|
||||
classifiers = [],
|
||||
classifiers=[],
|
||||
zip_safe=False,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user