1
0
mirror of https://github.com/ciromattia/kcc synced 2025-12-26 16:11:48 +00:00

Merge pull request #153 from ciromattia/dev

4.6.4
This commit is contained in:
Paweł Jastrzębski
2015-09-11 09:38:56 +02:00
16 changed files with 190 additions and 151 deletions

View File

@@ -32,10 +32,10 @@ You can find the latest released binary at the following links:
## 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.2.0+
- [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.2+
- [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:
@@ -156,6 +156,12 @@ The app relies and includes the following scripts:
* [Kobo Aura H2O](http://kcc.iosphe.re/Samples/Ubunchu-KoAH2O.kepub.epub)
## 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

View File

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

BIN
icons/WizardOSX.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

View File

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

View File

@@ -22,7 +22,6 @@ import sys
from urllib.parse import unquote
from urllib.request import urlopen, urlretrieve, Request
from socket import gethostbyname_ex, gethostname
from traceback import format_tb
from time import sleep, time
from datetime import datetime
from shutil import move
@@ -36,7 +35,7 @@ from copy import copy
from distutils.version import StrictVersion
from xml.sax.saxutils import escape
from platform import platform
from .shared import md5Checksum, HTMLStripper
from .shared import md5Checksum, HTMLStripper, sanitizeTrace
from . import __version__
from . import comic2ebook
from . import KCC_rc_web
@@ -67,7 +66,6 @@ class QApplicationMessaging(QtWidgets.QApplication):
socket.connectToServer(self._key, QtCore.QIODevice.WriteOnly)
if not socket.waitForConnected(self._timeout):
self._server = QtNetwork.QLocalServer(self)
# noinspection PyUnresolvedReferences
self._server.newConnection.connect(self.handleMessage)
self._server.listen(self._key)
else:
@@ -140,7 +138,7 @@ class Icons:
class WebServerHandler(BaseHTTPRequestHandler):
# noinspection PyAttributeOutsideInit, PyArgumentList
# noinspection PyAttributeOutsideInit
def do_GET(self):
if self.path == '/':
self.path = '/index.html'
@@ -277,8 +275,8 @@ class VersionThread(QtCore.QThread):
try:
MW.modeConvert.emit(-1)
MW.progressBarTick.emit('Downloading update')
path = urlretrieve('https://kcc.iosphe.re/Windows/KindleComicConverter_win_'
+ self.newVersion + '.exe', reporthook=self.getNewVersionTick)
path = urlretrieve('https://kcc.iosphe.re/Windows/KindleComicConverter_win_' +
self.newVersion + '.exe', reporthook=self.getNewVersionTick)
if self.md5 != md5Checksum(path[0]):
raise Exception
move(path[0], path[0] + '.exe')
@@ -325,7 +323,6 @@ class ProgressThread(QtCore.QThread):
class WorkerThread(QtCore.QThread):
# noinspection PyArgumentList
def __init__(self):
QtCore.QThread.__init__(self)
self.conversionAlive = False
@@ -350,12 +347,6 @@ class WorkerThread(QtCore.QThread):
MW.addTrayMessage.emit('Conversion interrupted.', 'Critical')
MW.modeConvert.emit(1)
def sanitizeTrace(self, traceback):
return ''.join(format_tb(traceback))\
.replace('C:\\Users\\pawel\\Documents\\Projekty\\KCC\\', '')\
.replace('C:\\Python34\\', '')\
.replace('C:\\Python34_64\\', '')
def run(self):
MW.modeConvert.emit(0)
@@ -439,16 +430,20 @@ class WorkerThread(QtCore.QThread):
GUI.progress.content = ''
self.errors = True
MW.addMessage.emit(str(warn), 'warning', False)
MW.addMessage.emit('Failed to create output file!', 'error', False)
MW.addTrayMessage.emit('Failed to create output file!', 'Critical')
MW.addMessage.emit('Error during conversion! Please consult '
'<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:
GUI.progress.content = ''
self.errors = True
_, _, traceback = sys.exc_info()
MW.showDialog.emit("Error during conversion %s:\n\n%s\n\nTraceback:\n%s"
% (jobargv[-1], str(err), self.sanitizeTrace(traceback)), 'error')
MW.addMessage.emit('Failed to create EPUB!', 'error', False)
MW.addTrayMessage.emit('Failed to create EPUB!', 'Critical')
% (jobargv[-1], str(err), sanitizeTrace(traceback)), 'error')
MW.addMessage.emit('Error during conversion! Please consult '
'<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:
for item in outputPath:
if os.path.exists(item):
@@ -463,7 +458,7 @@ class WorkerThread(QtCore.QThread):
MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True)
if str(GUI.FormatBox.currentText()) == 'MOBI':
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.addMessage.emit('Creating MOBI files', 'info', False)
GUI.progress.content = 'Creating MOBI files'
@@ -502,7 +497,7 @@ class WorkerThread(QtCore.QThread):
GUI.progress.content = ''
mobiPath = item.replace('.epub', '.mobi')
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:
move(mobiPath, GUI.targetDirectory)
mobiPath = os.path.join(GUI.targetDirectory, os.path.basename(mobiPath))
@@ -522,7 +517,7 @@ class WorkerThread(QtCore.QThread):
MW.addTrayMessage.emit('Failed to process MOBI file!', 'Critical')
else:
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:
if os.path.exists(item):
os.remove(item)
@@ -538,7 +533,7 @@ class WorkerThread(QtCore.QThread):
False)
else:
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:
move(item, GUI.targetDirectory)
item = os.path.join(GUI.targetDirectory, os.path.basename(item))
@@ -549,8 +544,9 @@ class WorkerThread(QtCore.QThread):
GUI.progress.stop()
MW.hideProgressBar.emit()
GUI.needClean = True
MW.addMessage.emit('<b>All jobs completed.</b>', 'info', False)
MW.addTrayMessage.emit('All jobs completed.', 'Information')
if not self.errors:
MW.addMessage.emit('<b>All jobs completed.</b>', 'info', False)
MW.addTrayMessage.emit('All jobs completed.', 'Information')
MW.modeConvert.emit(1)
@@ -559,7 +555,6 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
super().__init__()
if self.isSystemTrayAvailable():
QtWidgets.QSystemTrayIcon.__init__(self, GUI.icons.programIcon, MW)
# noinspection PyUnresolvedReferences
self.activated.connect(self.catchClicks)
def catchClicks(self):
@@ -635,8 +630,10 @@ class KCCGUI(KCC_ui.Ui_KCC):
self.lastPath = os.path.abspath(os.path.join(fname, os.pardir))
try:
self.editor.loadData(fname)
except:
self.showDialog('Failed to parse metadata!', 'error')
except Exception as err:
_, _, traceback = sys.exc_info()
self.showDialog("Failed to parse metadata!\n\n%s\n\nTraceback:\n%s"
% (str(err), sanitizeTrace(traceback)), 'error')
else:
self.editor.ui.exec_()
@@ -845,7 +842,7 @@ class KCCGUI(KCC_ui.Ui_KCC):
def changeGamma(self, value):
value = float(value)
value = '%.2f' % (value/100)
value = '%.2f' % (value / 100)
if float(value) <= 0.09:
GUI.GammaLabel.setText('Gamma: Auto')
else:
@@ -913,7 +910,7 @@ class KCCGUI(KCC_ui.Ui_KCC):
else:
item = QtWidgets.QListWidgetItem(' ' + self.stripTags(message))
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
# We still fill original text field with transparent content to trigger creation of horizontal scrollbar
item.setForeground(QtGui.QColor('transparent'))
@@ -1053,7 +1050,7 @@ class KCCGUI(KCC_ui.Ui_KCC):
'ColorBox': GUI.ColorBox.checkState(),
'customWidth': GUI.customWidth.text(),
'customHeight': GUI.customHeight.text(),
'GammaSlider': float(self.GammaValue)*100})
'GammaSlider': float(self.GammaValue) * 100})
self.settings.sync()
self.tray.hide()
@@ -1300,7 +1297,7 @@ class KCCGUI(KCC_ui.Ui_KCC):
if profile == "Other":
GUI.DeviceBox.addItem(self.icons.deviceOther, profile)
elif profile == "Separator":
GUI.DeviceBox.insertSeparator(GUI.DeviceBox.count()+1)
GUI.DeviceBox.insertSeparator(GUI.DeviceBox.count() + 1)
elif 'Ko' in profile:
GUI.DeviceBox.addItem(self.icons.deviceKobo, profile)
else:
@@ -1381,8 +1378,10 @@ class KCCGUI_MetaEditor(KCC_MetaEditor_ui.Ui_MetaEditorDialog):
self.parser.data[field.objectName()[:-4] + 's'] = tmpData
try:
self.parser.saveXML()
except:
GUI.showDialog('Failed to save metadata!', 'error')
except Exception as err:
_, _, 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()
def cleanData(self, s):

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

View File

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

View File

@@ -70,8 +70,8 @@ class CBxArchive:
if sys.platform.startswith('darwin'):
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')
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)
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)
extracted = False
for line in output.stdout:
if b"Everything is Ok" in line:

View File

@@ -28,7 +28,7 @@ from urllib.request import Request, urlopen
from re import sub
from stat import S_IWRITE, S_IREAD, S_IEXEC
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
from tempfile import mkdtemp
from tempfile import mkdtemp, gettempdir
from shutil import move, copytree, rmtree
from optparse import OptionParser, OptionGroup
from multiprocessing import Pool
@@ -43,7 +43,7 @@ try:
from PyQt5 import QtCore
except ImportError:
QtCore = None
from .shared import md5Checksum, getImageFileName, walkSort, walkLevel, saferReplace
from .shared import md5Checksum, getImageFileName, walkSort, walkLevel, saferReplace
from . import comic2panel
from . import image
from . import cbxarchive
@@ -66,6 +66,7 @@ def main(argv=None):
print('No matching files found.')
return
for source in sources:
source = source.rstrip('\\').rstrip('/')
options = copy(optionstemplate)
checkOptions()
if len(sources) > 1:
@@ -242,9 +243,9 @@ def buildNCX(dstdir, title, chapters, chapterNames):
navID = filename[0].replace('/', '_').replace('\\', '_')
elif os.path.basename(folder) != "Text":
title = chapterNames[os.path.basename(folder)]
f.write("<navPoint id=\"" + navID + "\"><navLabel><text>"
+ title + "</text></navLabel><content src=\"" + filename[0].replace("\\", "/")
+ ".html\"/></navPoint>\n")
f.write("<navPoint id=\"" + navID + "\"><navLabel><text>" +
title + "</text></navLabel><content src=\"" + filename[0].replace("\\", "/") +
".html\"/></navPoint>\n")
f.write("</navMap>\n</ncx>")
f.close()
@@ -341,15 +342,15 @@ def buildOPF(dstdir, title, filelist, cover=None):
filename = getImageFileName(path[1])
uniqueid = os.path.join(folder, filename[0]).replace('/', '_').replace('\\', '_')
reflist.append(uniqueid)
f.write("<item id=\"page_" + str(uniqueid) + "\" href=\""
+ folder.replace('Images', 'Text') + "/" + filename[0]
+ ".html\" media-type=\"application/xhtml+xml\"/>\n")
f.write("<item id=\"page_" + str(uniqueid) + "\" href=\"" +
folder.replace('Images', 'Text') + "/" + filename[0] +
".html\" media-type=\"application/xhtml+xml\"/>\n")
if '.png' == filename[1]:
mt = 'image/png'
else:
mt = 'image/jpeg'
f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\""
+ mt + "\"/>\n")
f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" +
mt + "\"/>\n")
f.write("<item id=\"css\" href=\"Text/style.css\" media-type=\"text/css\"/>\n")
if options.righttoleft:
f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n")
@@ -647,44 +648,38 @@ def imgFileProcessing(work):
def getWorkFolder(afile):
if len(afile) > 240:
raise UserWarning("Path is too long.")
if os.path.isdir(afile):
workdir = mkdtemp('', 'KCC-')
try:
os.rmdir(workdir)
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
if len(fullPath) > 240:
raise UserWarning("Path is too long.")
copytree(afile, fullPath)
sanitizePermissions(fullPath)
return workdir
except OSError:
except:
rmtree(workdir, True)
raise
raise UserWarning("Failed to prepare a workspace.")
elif afile.lower().endswith('.pdf'):
pdf = pdfjpgextract.PdfJpgExtract(afile)
path, njpg = pdf.extract()
if njpg == 0:
rmtree(path, True)
raise UserWarning("Failed to extract images.")
raise UserWarning("Failed to extract images from PDF file.")
else:
workdir = mkdtemp('', 'KCC-')
cbx = cbxarchive.CBxArchive(afile)
if cbx.isCbxFile():
try:
path = cbx.extract(workdir)
except OSError:
except:
rmtree(workdir, True)
raise UserWarning("Failed to extract file.")
raise UserWarning("Failed to extract archive.")
else:
rmtree(workdir, True)
raise TypeError("Failed to detect archive format.")
if len(os.path.join(path, 'OEBPS', 'Images')) > 240:
raise UserWarning("Path is too long.")
move(path, path + "_temp")
move(path + "_temp", os.path.join(path, 'OEBPS', 'Images'))
return path
raise UserWarning("Failed to detect archive format.")
newpath = mkdtemp('', 'KCC-')
move(path, os.path.join(newpath, 'OEBPS', 'Images'))
return newpath
def getOutputFilename(srcpath, wantedname, ext, tomeNumber):
@@ -1039,7 +1034,7 @@ def createNewTome():
def slugify(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
@@ -1181,7 +1176,7 @@ def checkOptions():
if options.customheight != 0:
Y = options.customheight
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
options.profile = "Custom"
options.profileData = image.ProfileData.Profiles[options.profile]
@@ -1208,6 +1203,21 @@ def checkTools(source):
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):
"""Generates MOBI/EPUB/CBZ comic ebook from a bunch of images."""
global GUI
@@ -1216,6 +1226,7 @@ def makeBook(source, qtGUI=None):
GUI.progressBarTick.emit('1')
else:
checkTools(source)
checkPre(source)
path = getWorkFolder(source)
print("\nChecking images...")
getComicInfo(os.path.join(path, "OEBPS", "Images"), source)
@@ -1348,7 +1359,7 @@ def makeMOBI(work, qtGUI=None):
global GUI, makeMOBIWorkerPool, makeMOBIWorkerOutput
GUI = qtGUI
makeMOBIWorkerOutput = []
availableMemory = virtual_memory().total/1000000000
availableMemory = virtual_memory().total / 1000000000
if availableMemory <= 2:
threadNumber = 1
elif 2 < availableMemory <= 4:

View File

@@ -85,19 +85,19 @@ 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[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[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)])

View File

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

View File

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

View File

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

View File

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

10
setup.json Normal file
View File

@@ -0,0 +1,10 @@
{
"title": "Kindle Comic Converter",
"icon": "icons/comic2ebook.icns",
"background": "icons/WizardOSX.png",
"icon-size": 160,
"contents": [
{ "x": 180, "y": 300, "type": "file", "path": "dist/Kindle Comic Converter.app" },
{ "x": 520, "y": 300, "type": "link", "path": "/Applications" }
]
}

View File

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