mirror of
https://github.com/ciromattia/kcc
synced 2025-12-13 17:56:30 +00:00
Remove obsolete droplet
This commit is contained in:
@@ -1,65 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleAllowMixedLocalizations</key>
|
|
||||||
<true/>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>English</string>
|
|
||||||
<key>CFBundleDocumentTypes</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>*</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeOSTypes</key>
|
|
||||||
<array>
|
|
||||||
<string>****</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>droplet</string>
|
|
||||||
<key>CFBundleGetInfoString</key>
|
|
||||||
<string>KindleComicConverter 2.0, Written 2012 by Ciro Mattia Gonano</string>
|
|
||||||
<key>CFBundleIconFile</key>
|
|
||||||
<string>droplet</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>com.github.ciromattia.kcc</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>KindleComicConverter 1.20</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>1.0</string>
|
|
||||||
<key>CFBundleSignature</key>
|
|
||||||
<string>dplt</string>
|
|
||||||
<key>LSMinimumSystemVersionByArchitecture</key>
|
|
||||||
<dict>
|
|
||||||
<key>x86_64</key>
|
|
||||||
<string>10.6</string>
|
|
||||||
</dict>
|
|
||||||
<key>LSRequiresCarbon</key>
|
|
||||||
<true/>
|
|
||||||
<key>WindowState</key>
|
|
||||||
<dict>
|
|
||||||
<key>dividerCollapsed</key>
|
|
||||||
<true/>
|
|
||||||
<key>eventLogLevel</key>
|
|
||||||
<integer>-1</integer>
|
|
||||||
<key>name</key>
|
|
||||||
<string>ScriptWindowState</string>
|
|
||||||
<key>positionOfDivider</key>
|
|
||||||
<real>568</real>
|
|
||||||
<key>savedFrame</key>
|
|
||||||
<string>144 338 889 690 0 0 1680 1028 </string>
|
|
||||||
<key>selectedTabView</key>
|
|
||||||
<string>event log</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
APPLdplt
|
|
||||||
Binary file not shown.
@@ -1,84 +0,0 @@
|
|||||||
# Copyright (c) 2012 Ciro Mattia Gonano <ciromattia@gmail.com>
|
|
||||||
#
|
|
||||||
# Permission to use, copy, modify, and/or distribute this software for
|
|
||||||
# any purpose with or without fee is hereby granted, provided that the
|
|
||||||
# above copyright notice and this permission notice appear in all
|
|
||||||
# copies.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
|
||||||
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
|
|
||||||
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
|
||||||
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
|
|
||||||
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
|
||||||
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
# PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
#
|
|
||||||
__license__ = 'ISC'
|
|
||||||
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
|
|
||||||
__docformat__ = 'restructuredtext en'
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
class CBxArchive:
|
|
||||||
def __init__(self, origFileName):
|
|
||||||
self.cbxexts = ['.zip','.cbz','.rar','.cbr']
|
|
||||||
self.origFileName = origFileName
|
|
||||||
self.filename = os.path.splitext(origFileName)
|
|
||||||
self.path = self.filename[0]
|
|
||||||
|
|
||||||
def isCbxFile(self):
|
|
||||||
result = (self.filename[1].lower() in self.cbxexts)
|
|
||||||
if result == True:
|
|
||||||
return result
|
|
||||||
return False
|
|
||||||
|
|
||||||
def getPath(self):
|
|
||||||
return self.path
|
|
||||||
|
|
||||||
def extractCBZ(self):
|
|
||||||
try:
|
|
||||||
from zipfile import ZipFile
|
|
||||||
except ImportError:
|
|
||||||
self.cbzFile = None
|
|
||||||
cbzFile = ZipFile(self.origFileName)
|
|
||||||
for f in cbzFile.namelist():
|
|
||||||
if (f.startswith('__MACOSX') or f.endswith('.DS_Store')):
|
|
||||||
pass # skip MacOS special files
|
|
||||||
elif f.endswith('/'):
|
|
||||||
try:
|
|
||||||
os.makedirs(self.path+'/'+f)
|
|
||||||
except:
|
|
||||||
pass #the dir exists so we are going to extract the images only.
|
|
||||||
else:
|
|
||||||
cbzFile.extract(f, self.path)
|
|
||||||
|
|
||||||
def extractCBR(self):
|
|
||||||
try:
|
|
||||||
import rarfile
|
|
||||||
except ImportError:
|
|
||||||
self.cbrFile = None
|
|
||||||
return
|
|
||||||
cbrFile = rarfile.RarFile(self.origFileName)
|
|
||||||
for f in cbrFile.namelist():
|
|
||||||
if (f.startswith('__MACOSX') or f.endswith('.DS_Store')):
|
|
||||||
pass # skip MacOS special files
|
|
||||||
elif f.endswith('/'):
|
|
||||||
try:
|
|
||||||
os.makedirs(self.path+'/'+f)
|
|
||||||
except:
|
|
||||||
pass #the dir exists so we are going to extract the images only.
|
|
||||||
else:
|
|
||||||
cbrFile.extract(f, self.path)
|
|
||||||
|
|
||||||
def extract(self):
|
|
||||||
if ('.cbr' == self.filename[1].lower() or '.rar' == self.filename[1].lower()):
|
|
||||||
self.extractCBR()
|
|
||||||
elif ('.cbz' == self.filename[1].lower() or '.zip' == self.filename[1].lower()):
|
|
||||||
self.extractCBZ()
|
|
||||||
dir = os.listdir(self.path)
|
|
||||||
if (len(dir) == 1):
|
|
||||||
import shutil
|
|
||||||
for f in os.listdir(self.path + "/" + dir[0]):
|
|
||||||
shutil.move(self.path + "/" + dir[0] + "/" + f,self.path)
|
|
||||||
os.rmdir(self.path + "/" + dir[0])
|
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright (c) 2012 Ciro Mattia Gonano <ciromattia@gmail.com>
|
|
||||||
#
|
|
||||||
# Permission to use, copy, modify, and/or distribute this software for
|
|
||||||
# any purpose with or without fee is hereby granted, provided that the
|
|
||||||
# above copyright notice and this permission notice appear in all
|
|
||||||
# copies.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
|
||||||
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
|
||||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
|
|
||||||
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
|
||||||
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
|
|
||||||
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
|
||||||
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
||||||
# PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
#
|
|
||||||
# Changelog
|
|
||||||
# 1.00 - Initial version
|
|
||||||
# 1.10 - Added support for CBZ/CBR files
|
|
||||||
# 1.11 - Added support for ZIP/RAR extensions
|
|
||||||
# 1.20 - Comic optimizations! Split pages not target-oriented (landscape
|
|
||||||
# with portrait target or portrait with landscape target), add palette
|
|
||||||
# and other image optimizations from Mangle.
|
|
||||||
# WARNING: PIL is required for all image mangling!
|
|
||||||
# 1.30 - Fixed an issue in OPF generation for device resolution
|
|
||||||
# Reworked options system (call with -h option to get the inline help)
|
|
||||||
# 1.40 - Added some options for controlling image optimization
|
|
||||||
# Further optimization (ImageOps, page numbering cut, autocontrast)
|
|
||||||
# 1.41 - Fixed a serious bug on resizing when img ratio was bigger than device one
|
|
||||||
# 1.50 - Support for subfolders
|
|
||||||
#
|
|
||||||
# Todo:
|
|
||||||
# - Add gracefully exit for CBR if no rarfile.py and no unrar
|
|
||||||
# executable are found
|
|
||||||
# - Improve error reporting
|
|
||||||
|
|
||||||
__version__ = '1.50'
|
|
||||||
__license__ = 'ISC'
|
|
||||||
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
|
|
||||||
__docformat__ = 'restructuredtext en'
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from optparse import OptionParser
|
|
||||||
import image, cbxarchive, pdfjpgextract
|
|
||||||
|
|
||||||
def buildHTML(path,file):
|
|
||||||
filename = getImageFileName(file)
|
|
||||||
if filename is not None:
|
|
||||||
htmlfile = os.path.join(path,filename[0] + '.html')
|
|
||||||
f = open(htmlfile, "w")
|
|
||||||
f.writelines(["<!DOCTYPE html SYSTEM \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n",
|
|
||||||
"<html xmlns=\"http://www.w3.org/1999/xhtml\">\n",
|
|
||||||
"<head>\n",
|
|
||||||
"<title>",filename[0],"</title>\n",
|
|
||||||
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n",
|
|
||||||
"</head>\n",
|
|
||||||
"<body>\n",
|
|
||||||
"<div><img src=\"",file,"\" /></div>\n",
|
|
||||||
"</body>\n",
|
|
||||||
"</html>"
|
|
||||||
])
|
|
||||||
f.close()
|
|
||||||
return path,file
|
|
||||||
|
|
||||||
def buildNCX(dstdir, title, chapters):
|
|
||||||
ncxfile = dstdir + '/toc.ncx'
|
|
||||||
f = open(ncxfile, "w")
|
|
||||||
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
|
|
||||||
"<!DOCTYPE ncx PUBLIC \"-//NISO//DTD ncx 2005-1//EN\" \"http://www.daisy.org/z3986/2005/ncx-2005-1.dtd\">\n",
|
|
||||||
"<ncx version=\"2005-1\" xml:lang=\"en-US\" xmlns=\"http://www.daisy.org/z3986/2005/ncx/\">\n",
|
|
||||||
"<head>\n</head>\n",
|
|
||||||
"<docTitle><text>",title,"</text></docTitle>\n",
|
|
||||||
"<navMap>"
|
|
||||||
])
|
|
||||||
for chapter in chapters:
|
|
||||||
folder = chapter[0].replace(dstdir,'').lstrip('/')
|
|
||||||
title = os.path.basename(folder)
|
|
||||||
filename = getImageFileName(os.path.join(folder,chapter[1]))
|
|
||||||
f.write("<navPoint id=\"" + folder + "\"><navLabel><text>" + title
|
|
||||||
+ "</text></navLabel><content src=\"" + filename[0] + ".html\"/></navPoint>\n")
|
|
||||||
f.write("</navMap>\n</ncx>")
|
|
||||||
f.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
def buildOPF(profile, dstdir, title, filelist, cover=None):
|
|
||||||
opffile = dstdir + '/content.opf'
|
|
||||||
# read the first file resolution
|
|
||||||
profilelabel, deviceres, palette = image.ProfileData.Profiles[profile]
|
|
||||||
imgres = str(deviceres[0]) + "x" + str(deviceres[1])
|
|
||||||
f = open(opffile, "w")
|
|
||||||
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
|
|
||||||
"<package version=\"2.0\" unique-identifier=\"PrimaryID\" xmlns=\"http://www.idpf.org/2007/opf\">\n",
|
|
||||||
"<metadata xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:opf=\"http://www.idpf.org/2007/opf\">\n",
|
|
||||||
"<dc:title>",title,"</dc:title>\n",
|
|
||||||
"<dc:language>en-US</dc:language>\n",
|
|
||||||
"<meta name=\"cover\" content=\"cover\"/>\n",
|
|
||||||
"<meta name=\"book-type\" content=\"comic\"/>\n",
|
|
||||||
"<meta name=\"zero-gutter\" content=\"true\"/>\n",
|
|
||||||
"<meta name=\"zero-margin\" content=\"true\"/>\n",
|
|
||||||
"<meta name=\"fixed-layout\" content=\"true\"/>\n",
|
|
||||||
"<meta name=\"orientation-lock\" content=\"portrait\"/>\n",
|
|
||||||
"<meta name=\"original-resolution\" content=\"" + imgres + "\"/>\n",
|
|
||||||
"</metadata>\n<manifest>\n<item id=\"ncx\" href=\"toc.ncx\" media-type=\"application/x-dtbncx+xml\"/>\n"
|
|
||||||
])
|
|
||||||
# set cover
|
|
||||||
if cover is not None:
|
|
||||||
folder = cover[0].replace(dstdir,'').lstrip('/')
|
|
||||||
filename = getImageFileName(cover[1])
|
|
||||||
if '.png' == filename[1]:
|
|
||||||
mt = 'image/png'
|
|
||||||
else:
|
|
||||||
mt = 'image/jpeg'
|
|
||||||
f.write("<item id=\"cover\" href=\"" + os.path.join(folder,cover[1]) + "\" media-type=\"" + mt + "\"/>\n")
|
|
||||||
for path in filelist:
|
|
||||||
folder = path[0].replace(dstdir,'').lstrip('/')
|
|
||||||
filename = getImageFileName(path[1])
|
|
||||||
uniqueid = os.path.join(folder,filename[0]).replace('/','_')
|
|
||||||
f.write("<item id=\"page_" + uniqueid + "\" href=\"" + os.path.join(folder,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_" + uniqueid + "\" href=\"" + os.path.join(folder,path[1]) + "\" media-type=\"" + mt + "\"/>\n")
|
|
||||||
f.write("</manifest>\n<spine toc=\"ncx\">\n")
|
|
||||||
for path in filelist:
|
|
||||||
folder = path[0].replace(dstdir,'').lstrip('/')
|
|
||||||
filename = getImageFileName(path[1])
|
|
||||||
uniqueid = os.path.join(folder,filename[0]).replace('/','_')
|
|
||||||
f.write("<itemref idref=\"page_" + uniqueid + "\" />\n")
|
|
||||||
f.write("</spine>\n<guide>\n</guide>\n</package>\n")
|
|
||||||
f.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
def getImageFileName(file):
|
|
||||||
filename = os.path.splitext(file)
|
|
||||||
if filename[0].startswith('.') or (filename[1].lower() != '.png' and filename[1].lower() != '.jpg' and filename[1].lower() != '.jpeg'):
|
|
||||||
return None
|
|
||||||
return filename
|
|
||||||
|
|
||||||
def isInFilelist(file,list):
|
|
||||||
filename = os.path.splitext(file)
|
|
||||||
seen = False
|
|
||||||
for item in list:
|
|
||||||
if filename[0] == item[0]:
|
|
||||||
seen = True
|
|
||||||
return seen
|
|
||||||
|
|
||||||
def applyImgOptimization(img):
|
|
||||||
img.optimizeImage()
|
|
||||||
img.cropWhiteSpace(10.0)
|
|
||||||
if options.cutpagenumbers:
|
|
||||||
img.cutPageNumber()
|
|
||||||
img.resizeImage(options.upscale,options.stretch)
|
|
||||||
img.quantizeImage()
|
|
||||||
|
|
||||||
|
|
||||||
def dirImgProcess(path):
|
|
||||||
global options
|
|
||||||
|
|
||||||
for (dirpath, dirnames, filenames) in os.walk(path):
|
|
||||||
for file in filenames:
|
|
||||||
if getImageFileName(file) is not None:
|
|
||||||
if options.verbose:
|
|
||||||
print "Optimizing " + file + " for " + options.profile
|
|
||||||
else:
|
|
||||||
print ".",
|
|
||||||
img = image.ComicPage(os.path.join(dirpath,file), options.profile)
|
|
||||||
split = img.splitPage(dirpath, options.righttoleft)
|
|
||||||
if split is not None:
|
|
||||||
if options.verbose:
|
|
||||||
print "Splitted " + file
|
|
||||||
img0 = image.ComicPage(split[0],options.profile)
|
|
||||||
img1 = image.ComicPage(split[1],options.profile)
|
|
||||||
applyImgOptimization(img0)
|
|
||||||
img0.saveToDir(dirpath)
|
|
||||||
applyImgOptimization(img1)
|
|
||||||
img1.saveToDir(dirpath)
|
|
||||||
else:
|
|
||||||
applyImgOptimization(img)
|
|
||||||
img.saveToDir(dirpath)
|
|
||||||
|
|
||||||
def genEpubStruct(path):
|
|
||||||
global options
|
|
||||||
filelist = []
|
|
||||||
chapterlist = []
|
|
||||||
cover = None
|
|
||||||
for (dirpath, dirnames, filenames) in os.walk(path):
|
|
||||||
chapter = False
|
|
||||||
for file in filenames:
|
|
||||||
if getImageFileName(file) is not None:
|
|
||||||
# put credits at the end
|
|
||||||
if "credits" in file.lower():
|
|
||||||
os.rename(os.path.join(dirpath,file), os.path.join(dirpath,'ZZZ999_'+file))
|
|
||||||
file = 'ZZZ999_'+file
|
|
||||||
filelist.append(buildHTML(dirpath,file))
|
|
||||||
if not chapter:
|
|
||||||
chapterlist.append((dirpath,filelist[-1][1]))
|
|
||||||
chapter = True
|
|
||||||
if cover is None:
|
|
||||||
cover = filelist[-1]
|
|
||||||
if options.title == 'defaulttitle':
|
|
||||||
options.title = os.path.basename(path)
|
|
||||||
buildNCX(path,options.title,chapterlist)
|
|
||||||
# ensure we're sorting files alphabetically
|
|
||||||
filelist = sorted(filelist, key=lambda name: (name[0].lower(), name[1].lower()))
|
|
||||||
buildOPF(options.profile,path,options.title,filelist,cover)
|
|
||||||
|
|
||||||
def getWorkFolder(file):
|
|
||||||
fname = os.path.splitext(file)
|
|
||||||
if fname[1].lower() == '.pdf':
|
|
||||||
pdf = pdfjpgextract.PdfJpgExtract(file)
|
|
||||||
pdf.extract()
|
|
||||||
return pdf.getPath()
|
|
||||||
else:
|
|
||||||
cbx = cbxarchive.CBxArchive(file)
|
|
||||||
if cbx.isCbxFile():
|
|
||||||
cbx.extract()
|
|
||||||
return cbx.getPath()
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
import shutil
|
|
||||||
if not os.path.isdir(file + "_orig"):
|
|
||||||
shutil.copytree(file, file + "_orig")
|
|
||||||
return file
|
|
||||||
except OSError:
|
|
||||||
raise
|
|
||||||
|
|
||||||
def Copyright():
|
|
||||||
print ('comic2ebook v%(__version__)s. '
|
|
||||||
'Written 2012 by Ciro Mattia Gonano.' % globals())
|
|
||||||
|
|
||||||
def Usage():
|
|
||||||
print "Generates HTML, NCX and OPF for a Comic ebook from a bunch of images"
|
|
||||||
print "Optimized for creating Mobipockets to be read into Kindle Paperwhite"
|
|
||||||
parser.print_help()
|
|
||||||
|
|
||||||
def main(argv=None):
|
|
||||||
global parser, options, epub_path
|
|
||||||
usage = "Usage: %prog [options] comic_file|comic_folder"
|
|
||||||
parser = OptionParser(usage=usage, version=__version__)
|
|
||||||
parser.add_option("-p", "--profile", action="store", dest="profile", default="KHD",
|
|
||||||
help="Device profile (choose one among K1, K2, K3, K4, KDX, KDXG or KHD) [default=KHD]")
|
|
||||||
parser.add_option("-t", "--title", action="store", dest="title", default="defaulttitle",
|
|
||||||
help="Comic title [default=filename]")
|
|
||||||
parser.add_option("-m", "--manga-style", action="store_true", dest="righttoleft", default=False,
|
|
||||||
help="Split pages 'manga style' (right-to-left reading) [default=False]")
|
|
||||||
parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False,
|
|
||||||
help="Verbose output [default=False]")
|
|
||||||
parser.add_option("--no-image-processing", action="store_false", dest="imgproc", default=True,
|
|
||||||
help="Do not apply image preprocessing (page splitting and optimizations) [default=True]")
|
|
||||||
parser.add_option("--upscale-images", action="store_true", dest="upscale", default=False,
|
|
||||||
help="Resize images smaller than device's resolution [default=False]")
|
|
||||||
parser.add_option("--stretch-images", action="store_true", dest="stretch", default=False,
|
|
||||||
help="Stretch images to device's resolution [default=False]")
|
|
||||||
parser.add_option("--no-cut-page-numbers", action="store_false", dest="cutpagenumbers", default=True,
|
|
||||||
help="Do not try to cut page numbering on images [default=True]")
|
|
||||||
options, args = parser.parse_args(argv)
|
|
||||||
if len(args) != 1:
|
|
||||||
parser.print_help()
|
|
||||||
return
|
|
||||||
path = args[0]
|
|
||||||
path = getWorkFolder(path)
|
|
||||||
if options.imgproc:
|
|
||||||
print "Processing images..."
|
|
||||||
dirImgProcess(path)
|
|
||||||
print "Creating ePub structure..."
|
|
||||||
genEpubStruct(path)
|
|
||||||
epub_path = path
|
|
||||||
|
|
||||||
def getEpubPath():
|
|
||||||
global epub_path
|
|
||||||
return epub_path
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
Copyright()
|
|
||||||
main(sys.argv[1:])
|
|
||||||
sys.exit(0)
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
|
|
||||||
{\fonttbl\f0\fnil\fcharset0 Verdana;}
|
|
||||||
{\colortbl;\red255\green255\blue255;\red76\green78\blue78;}
|
|
||||||
\pard\tx576\tx1152\tx1728\tx2304\tx2880\tx3456\tx4032\tx4608\tx5184\tx5760\tx6337\tx6913\tx7489\tx8065\tx8641\tx9217\tx9793\tx10369\tx10945\tx11521\tx12097\tx12674\tx13250\tx13826\tx14402\tx14978\tx15554\tx16130\tx16706\tx17282\tx17858\tx18435\tx19011\tx19587\tx20163\tx20739\tx21315\tx21891\tx22467\tx23043\tx23619\tx24195\tx24772\tx25348\tx25924\tx26500\tx27076\tx27652\tx28228\tx28804\tx29380\tx29956\tx30532\tx31109\tx31685\tx32261\tx32837\tx33413\tx33989\tx34565\tx35141\tx35717\tx36293\tx36870\tx37446\tx38022\tx38598\tx39174\tx39750\tx40326\tx40902\tx41478\tx42054\tx42630\tx43207\tx43783\tx44359\tx44935\tx45511\tx46087\tx46663\tx47239\tx47815\tx48391\tx48967\tx49544\tx50120\tx50696\tx51272\tx51848\tx52424\tx53000\tx53576\tx54152\tx54728\tx55305\tx55881\tx56457\tx57033\tx57609\li785\fi-786\pardirnatural
|
|
||||||
|
|
||||||
\f0\fs24 \cf2 \CocoaLigature0 Copyright (c) 2012 Ciro Mattia Gonano <ciromattia@gmail.com>\
|
|
||||||
\
|
|
||||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\
|
|
||||||
\
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\
|
|
||||||
\
|
|
||||||
This script heavily relies on KindleStrip (C) by Paul Durrant and released in public domain (http://www.mobileread.com/forums/showthread.php?t=96903)\
|
|
||||||
Also, you need to have kindlegen v2.7 (with KF8 support) which is downloadable from Amazon website.\
|
|
||||||
\
|
|
||||||
Changelog:\
|
|
||||||
1.0: first release\
|
|
||||||
1.10: add CBZ/CBR support to comic2ebook.py\
|
|
||||||
1.11: add CBZ/CBR support to KindleComicConverter\
|
|
||||||
1.2: added image page splitting and optimizations\
|
|
||||||
\
|
|
||||||
Todo:\
|
|
||||||
- bundle a script to manipulate images (to get rid of Mangle/E-nki/whatsoever)}
|
|
||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 362 B |
@@ -1,331 +0,0 @@
|
|||||||
# Copyright (C) 2010 Alex Yatskov
|
|
||||||
# Copyright (C) 2011 Stanislav (proDOOMman) Kosolapov <prodoomman@gmail.com>
|
|
||||||
# Copyright (C) 2012-2013 Ciro Mattia Gonano <ciromattia@gmail.com>
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
__license__ = 'ISC'
|
|
||||||
__copyright__ = '2012-2013, Ciro Mattia Gonano <ciromattia@gmail.com>'
|
|
||||||
__docformat__ = 'restructuredtext en'
|
|
||||||
|
|
||||||
import os
|
|
||||||
from PIL import Image, ImageOps, ImageDraw, ImageStat
|
|
||||||
|
|
||||||
class ImageFlags:
|
|
||||||
Orient = 1 << 0
|
|
||||||
Resize = 1 << 1
|
|
||||||
Frame = 1 << 2
|
|
||||||
Quantize = 1 << 3
|
|
||||||
Stretch = 1 << 4
|
|
||||||
|
|
||||||
|
|
||||||
class ProfileData:
|
|
||||||
Palette4 = [
|
|
||||||
0x00, 0x00, 0x00,
|
|
||||||
0x55, 0x55, 0x55,
|
|
||||||
0xaa, 0xaa, 0xaa,
|
|
||||||
0xff, 0xff, 0xff
|
|
||||||
]
|
|
||||||
|
|
||||||
Palette15 = [
|
|
||||||
0x00, 0x00, 0x00,
|
|
||||||
0x11, 0x11, 0x11,
|
|
||||||
0x22, 0x22, 0x22,
|
|
||||||
0x33, 0x33, 0x33,
|
|
||||||
0x44, 0x44, 0x44,
|
|
||||||
0x55, 0x55, 0x55,
|
|
||||||
0x66, 0x66, 0x66,
|
|
||||||
0x77, 0x77, 0x77,
|
|
||||||
0x88, 0x88, 0x88,
|
|
||||||
0x99, 0x99, 0x99,
|
|
||||||
0xaa, 0xaa, 0xaa,
|
|
||||||
0xbb, 0xbb, 0xbb,
|
|
||||||
0xcc, 0xcc, 0xcc,
|
|
||||||
0xdd, 0xdd, 0xdd,
|
|
||||||
0xff, 0xff, 0xff,
|
|
||||||
]
|
|
||||||
|
|
||||||
Palette16 = [
|
|
||||||
0x00, 0x00, 0x00,
|
|
||||||
0x11, 0x11, 0x11,
|
|
||||||
0x22, 0x22, 0x22,
|
|
||||||
0x33, 0x33, 0x33,
|
|
||||||
0x44, 0x44, 0x44,
|
|
||||||
0x55, 0x55, 0x55,
|
|
||||||
0x66, 0x66, 0x66,
|
|
||||||
0x77, 0x77, 0x77,
|
|
||||||
0x88, 0x88, 0x88,
|
|
||||||
0x99, 0x99, 0x99,
|
|
||||||
0xaa, 0xaa, 0xaa,
|
|
||||||
0xbb, 0xbb, 0xbb,
|
|
||||||
0xcc, 0xcc, 0xcc,
|
|
||||||
0xdd, 0xdd, 0xdd,
|
|
||||||
0xee, 0xee, 0xee,
|
|
||||||
0xff, 0xff, 0xff,
|
|
||||||
]
|
|
||||||
|
|
||||||
Profiles = {
|
|
||||||
'K1': ("Kindle", (600, 800), Palette4),
|
|
||||||
'K2': ("Kindle 2", (600, 800), Palette15),
|
|
||||||
'K3': ("Kindle 3/Keyboard", (600, 800), Palette16),
|
|
||||||
'K4': ("Kindle 4/NT/Touch", (600, 800), Palette16),
|
|
||||||
'KHD': ("Kindle Paperwhite", (758, 1024), Palette16),
|
|
||||||
'KDX': ("Kindle DX", (824, 1200), Palette15),
|
|
||||||
'KDXG': ("Kindle DXG", (824, 1200), Palette16)
|
|
||||||
}
|
|
||||||
|
|
||||||
class ComicPage:
|
|
||||||
def __init__(self,source,device):
|
|
||||||
try:
|
|
||||||
self.profile_label, self.size, self.palette = ProfileData.Profiles[device]
|
|
||||||
except KeyError:
|
|
||||||
raise RuntimeError('Unexpected output device %s' % device)
|
|
||||||
try:
|
|
||||||
self.origFileName = source
|
|
||||||
self.image = Image.open(source)
|
|
||||||
except IOError:
|
|
||||||
raise RuntimeError('Cannot read image file %s' % source)
|
|
||||||
self.image = self.image.convert('RGB')
|
|
||||||
|
|
||||||
def saveToDir(self,targetdir):
|
|
||||||
filename = os.path.basename(self.origFileName)
|
|
||||||
#print "Saving to " + targetdir + '/' + filename
|
|
||||||
try:
|
|
||||||
self.image = self.image.convert('L') # convert to grayscale
|
|
||||||
self.image.save(targetdir + '/' + filename,"JPEG")
|
|
||||||
except IOError as e:
|
|
||||||
raise RuntimeError('Cannot write image in directory %s: %s' %(targetdir,e))
|
|
||||||
|
|
||||||
def optimizeImage(self):
|
|
||||||
self.image = ImageOps.autocontrast(self.image)
|
|
||||||
|
|
||||||
def quantizeImage(self):
|
|
||||||
colors = len(self.palette) / 3
|
|
||||||
if colors < 256:
|
|
||||||
self.palette = self.palette + self.palette[:3] * (256 - colors)
|
|
||||||
palImg = Image.new('P', (1, 1))
|
|
||||||
palImg.putpalette(self.palette)
|
|
||||||
self.image = self.image.quantize(palette=palImg)
|
|
||||||
|
|
||||||
def resizeImage(self,upscale=False, stretch=False):
|
|
||||||
method = Image.ANTIALIAS
|
|
||||||
if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]:
|
|
||||||
if not upscale:
|
|
||||||
# do not upscale but center image in a device-sized image
|
|
||||||
newImage = Image.new('RGB', (self.size[0], self.size[1]), (255,255,255))
|
|
||||||
newImage.paste(self.image, (
|
|
||||||
(self.size[0] - self.image.size[0]) / 2,
|
|
||||||
(self.size[1] - self.image.size[1]) / 2))
|
|
||||||
self.image = newImage
|
|
||||||
return self.image
|
|
||||||
else:
|
|
||||||
method = Image.NEAREST
|
|
||||||
|
|
||||||
if stretch: # if stretching call directly resize() without other considerations.
|
|
||||||
self.image = self.image.resize(self.size,method)
|
|
||||||
return self.image
|
|
||||||
|
|
||||||
ratioDev = float(self.size[0]) / float(self.size[1])
|
|
||||||
if (float(self.image.size[0]) / float(self.image.size[1])) < ratioDev:
|
|
||||||
diff = int(self.image.size[1] * ratioDev) - self.image.size[0]
|
|
||||||
newImage = Image.new('RGB', (self.image.size[0] + diff, self.image.size[1]), (255,255,255))
|
|
||||||
newImage.paste(self.image, (diff / 2, 0, diff / 2 + self.image.size[0], self.image.size[1]))
|
|
||||||
self.image = newImage
|
|
||||||
elif (float(self.image.size[0]) / float(self.image.size[1])) > ratioDev:
|
|
||||||
diff = int(self.image.size[0] / ratioDev) - self.image.size[1]
|
|
||||||
newImage = Image.new('RGB', (self.image.size[0], self.image.size[1] + diff), (255,255,255))
|
|
||||||
newImage.paste(self.image, (0, diff / 2, self.image.size[0], diff / 2 + self.image.size[1]))
|
|
||||||
self.image = newImage
|
|
||||||
self.image = ImageOps.fit(self.image, self.size, method = method, centering = (0.5,0.5))
|
|
||||||
return self.image
|
|
||||||
|
|
||||||
def splitPage(self, targetdir, righttoleft=False):
|
|
||||||
width, height = self.image.size
|
|
||||||
dstwidth, dstheight = self.size
|
|
||||||
#print "Image is %d x %d" % (width,height)
|
|
||||||
# only split if origin is not oriented the same as target
|
|
||||||
if (width > height) != (dstwidth > dstheight):
|
|
||||||
if width > height:
|
|
||||||
# source is landscape, so split by the width
|
|
||||||
leftbox = (0, 0, width/2, height)
|
|
||||||
rightbox = (width/2, 0, width, height)
|
|
||||||
else:
|
|
||||||
# source is portrait and target is landscape, so split by the height
|
|
||||||
leftbox = (0, 0, width, height/2)
|
|
||||||
rightbox = (0, height/2, width, height)
|
|
||||||
filename = os.path.splitext(os.path.basename(self.origFileName))
|
|
||||||
fileone = targetdir + '/' + filename[0] + '-1' + filename[1]
|
|
||||||
filetwo = targetdir + '/' + filename[0] + '-2' + filename[1]
|
|
||||||
try:
|
|
||||||
if righttoleft:
|
|
||||||
pageone = self.image.crop(rightbox)
|
|
||||||
pagetwo = self.image.crop(leftbox)
|
|
||||||
else:
|
|
||||||
pageone = self.image.crop(leftbox)
|
|
||||||
pagetwo = self.image.crop(rightbox)
|
|
||||||
pageone.save(fileone)
|
|
||||||
pagetwo.save(filetwo)
|
|
||||||
os.remove(self.origFileName)
|
|
||||||
except IOError as e:
|
|
||||||
raise RuntimeError('Cannot write image in directory %s: %s' %(targetdir,e))
|
|
||||||
return fileone,filetwo
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def frameImage(self):
|
|
||||||
foreground = tuple(self.palette[:3])
|
|
||||||
background = tuple(self.palette[-3:])
|
|
||||||
widthDev, heightDev = self.size
|
|
||||||
widthImg, heightImg = self.image.size
|
|
||||||
pastePt = (
|
|
||||||
max(0, (widthDev - widthImg) / 2),
|
|
||||||
max(0, (heightDev - heightImg) / 2)
|
|
||||||
)
|
|
||||||
corner1 = (
|
|
||||||
pastePt[0] - 1,
|
|
||||||
pastePt[1] - 1
|
|
||||||
)
|
|
||||||
corner2 = (
|
|
||||||
pastePt[0] + widthImg + 1,
|
|
||||||
pastePt[1] + heightImg + 1
|
|
||||||
)
|
|
||||||
imageBg = Image.new(self.image.mode, self.size, background)
|
|
||||||
imageBg.paste(self.image, pastePt)
|
|
||||||
draw = ImageDraw.Draw(imageBg)
|
|
||||||
draw.rectangle([corner1, corner2], outline=foreground)
|
|
||||||
self.image = imageBg
|
|
||||||
|
|
||||||
|
|
||||||
def cutPageNumber(self):
|
|
||||||
widthImg, heightImg = self.image.size
|
|
||||||
delta = 2
|
|
||||||
diff = delta
|
|
||||||
fixedThreshold = 5
|
|
||||||
if ImageStat.Stat(self.image).var[0] < 2*fixedThreshold:
|
|
||||||
return self.image
|
|
||||||
while ImageStat.Stat(self.image.crop((0,heightImg-diff,widthImg,heightImg))).var[0] < fixedThreshold\
|
|
||||||
and diff < heightImg:
|
|
||||||
diff += delta
|
|
||||||
diff -= delta
|
|
||||||
pageNumberCut1 = diff
|
|
||||||
if diff<delta:
|
|
||||||
diff=delta
|
|
||||||
oldStat=ImageStat.Stat(self.image.crop((0,heightImg-diff,widthImg,heightImg))).var[0]
|
|
||||||
diff += delta
|
|
||||||
while ImageStat.Stat(self.image.crop((0,heightImg-diff,widthImg,heightImg))).var[0] - oldStat > 0\
|
|
||||||
and diff < heightImg/4:
|
|
||||||
oldStat=ImageStat.Stat(self.image.crop((0,heightImg-diff,widthImg,heightImg))).var[0]
|
|
||||||
diff += delta
|
|
||||||
diff -= delta
|
|
||||||
pageNumberCut2 = diff
|
|
||||||
diff += delta
|
|
||||||
oldStat=ImageStat.Stat(self.image.crop((0,heightImg-diff,widthImg,heightImg-pageNumberCut2))).var[0]
|
|
||||||
while ImageStat.Stat(self.image.crop((0,heightImg-diff,widthImg,heightImg-pageNumberCut2))).var[0] < fixedThreshold+oldStat\
|
|
||||||
and diff < heightImg/4:
|
|
||||||
diff += delta
|
|
||||||
diff -= delta
|
|
||||||
pageNumberCut3 = diff
|
|
||||||
delta = 5
|
|
||||||
diff = delta
|
|
||||||
while ImageStat.Stat(self.image.crop((0,heightImg-pageNumberCut2,diff,heightImg))).var[0] < fixedThreshold and diff < widthImg:
|
|
||||||
diff += delta
|
|
||||||
diff -= delta
|
|
||||||
pageNumberX1 = diff
|
|
||||||
diff = delta
|
|
||||||
while ImageStat.Stat(self.image.crop((widthImg-diff,heightImg-pageNumberCut2,widthImg,heightImg))).var[0] < fixedThreshold and diff < widthImg:
|
|
||||||
diff += delta
|
|
||||||
diff -= delta
|
|
||||||
pageNumberX2=widthImg-diff
|
|
||||||
|
|
||||||
if pageNumberCut3-pageNumberCut1 > 2*delta\
|
|
||||||
and float(pageNumberX2-pageNumberX1)/float(pageNumberCut2-pageNumberCut1) <= 9.0\
|
|
||||||
and ImageStat.Stat(self.image.crop((0,heightImg-pageNumberCut3,widthImg,heightImg))).var[0] / ImageStat.Stat(self.image).var[0] < 0.1\
|
|
||||||
and pageNumberCut3 < heightImg/4-delta:
|
|
||||||
diff=pageNumberCut3
|
|
||||||
else:
|
|
||||||
diff=pageNumberCut1
|
|
||||||
self.image = self.image.crop((0,0,widthImg,heightImg-diff))
|
|
||||||
return self.image
|
|
||||||
|
|
||||||
def cropWhiteSpace(self, threshold):
|
|
||||||
widthImg, heightImg = self.image.size
|
|
||||||
delta = 10
|
|
||||||
diff = delta
|
|
||||||
# top
|
|
||||||
while ImageStat.Stat(self.image.crop((0,0,widthImg,diff))).var[0] < threshold and diff < heightImg:
|
|
||||||
diff += delta
|
|
||||||
diff -= delta
|
|
||||||
# print "Top crop: %s"%diff
|
|
||||||
self.image = self.image.crop((0,diff,widthImg,heightImg))
|
|
||||||
widthImg, heightImg = self.image.size
|
|
||||||
diff = delta
|
|
||||||
# left
|
|
||||||
while ImageStat.Stat(self.image.crop((0,0,diff,heightImg))).var[0] < threshold and diff < widthImg:
|
|
||||||
diff += delta
|
|
||||||
diff -= delta
|
|
||||||
# print "Left crop: %s"%diff
|
|
||||||
self.image = self.image.crop((diff,0,widthImg,heightImg))
|
|
||||||
widthImg, heightImg = self.image.size
|
|
||||||
diff = delta
|
|
||||||
# down
|
|
||||||
while ImageStat.Stat(self.image.crop((0,heightImg-diff,widthImg,heightImg))).var[0] < threshold\
|
|
||||||
and diff < heightImg:
|
|
||||||
diff += delta
|
|
||||||
diff -= delta
|
|
||||||
# print "Down crop: %s"%diff
|
|
||||||
self.image = self.image.crop((0,0,widthImg,heightImg-diff))
|
|
||||||
widthImg, heightImg = self.image.size
|
|
||||||
diff = delta
|
|
||||||
# right
|
|
||||||
while ImageStat.Stat(self.image.crop((widthImg-diff,0,widthImg,heightImg))).var[0] < threshold\
|
|
||||||
and diff < widthImg:
|
|
||||||
diff += delta
|
|
||||||
diff -= delta
|
|
||||||
# print "Right crop: %s"%diff
|
|
||||||
self.image = self.image.crop((0,0 ,widthImg-diff,heightImg))
|
|
||||||
# print "New size: %sx%s"%(self.image.size[0],self.image.size[1])
|
|
||||||
return self.image
|
|
||||||
|
|
||||||
def addProgressbar(self, file_number, files_totalnumber, size, howoften):
|
|
||||||
if file_number//howoften!=float(file_number)/howoften:
|
|
||||||
return self.image
|
|
||||||
white = (255,255,255)
|
|
||||||
black = (0,0,0)
|
|
||||||
widthDev, heightDev = size
|
|
||||||
widthImg, heightImg = self.image.size
|
|
||||||
pastePt = (
|
|
||||||
max(0, (widthDev - widthImg) / 2),
|
|
||||||
max(0, (heightDev - heightImg) / 2)
|
|
||||||
)
|
|
||||||
imageBg = Image.new('RGB',size,white)
|
|
||||||
imageBg.paste(self.image, pastePt)
|
|
||||||
self.image = imageBg
|
|
||||||
widthImg, heightImg = self.image.size
|
|
||||||
draw = ImageDraw.Draw(self.image)
|
|
||||||
#Black rectangle
|
|
||||||
draw.rectangle([(0,heightImg-3), (widthImg,heightImg)], outline=black, fill=black)
|
|
||||||
#White rectangle
|
|
||||||
draw.rectangle([(widthImg*file_number/files_totalnumber,heightImg-3), (widthImg-1,heightImg)], outline=black, fill=white)
|
|
||||||
#Making notches
|
|
||||||
for i in range(1,10):
|
|
||||||
if i <= (10*file_number/files_totalnumber):
|
|
||||||
notch_colour=white #White
|
|
||||||
else:
|
|
||||||
notch_colour=black #Black
|
|
||||||
draw.line([(widthImg*float(i)/10,heightImg-3), (widthImg*float(i)/10,heightImg)],fill=notch_colour)
|
|
||||||
#The 50%
|
|
||||||
if i==5:
|
|
||||||
draw.rectangle([(widthImg/2-1,heightImg-5), (widthImg/2+1,heightImg)],outline=black,fill=notch_colour)
|
|
||||||
return self.image
|
|
||||||
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
|
||||||
#
|
|
||||||
# This is a python script. You need a Python interpreter to run it.
|
|
||||||
# For example, ActiveState Python, which exists for windows.
|
|
||||||
#
|
|
||||||
# This script strips the penultimate record from a Mobipocket file.
|
|
||||||
# This is useful because the current KindleGen add a compressed copy
|
|
||||||
# of the source files used in this record, making the ebook produced
|
|
||||||
# about twice as big as it needs to be.
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# This is free and unencumbered software released into the public domain.
|
|
||||||
#
|
|
||||||
# Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
||||||
# distribute this software, either in source code form or as a compiled
|
|
||||||
# binary, for any purpose, commercial or non-commercial, and by any
|
|
||||||
# means.
|
|
||||||
#
|
|
||||||
# In jurisdictions that recognize copyright laws, the author or authors
|
|
||||||
# of this software dedicate any and all copyright interest in the
|
|
||||||
# software to the public domain. We make this dedication for the benefit
|
|
||||||
# of the public at large and to the detriment of our heirs and
|
|
||||||
# successors. We intend this dedication to be an overt act of
|
|
||||||
# relinquishment in perpetuity of all present and future rights to this
|
|
||||||
# software under copyright law.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
||||||
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
||||||
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
||||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
# OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
#
|
|
||||||
# For more information, please refer to <http://unlicense.org/>
|
|
||||||
#
|
|
||||||
# Written by Paul Durrant, 2010-2011, paul@durrant.co.uk, pdurrant on mobileread.com
|
|
||||||
# With enhancements by Kevin Hendricks, KevinH on mobileread.com
|
|
||||||
#
|
|
||||||
# Changelog
|
|
||||||
# 1.00 - Initial version
|
|
||||||
# 1.10 - Added an option to output the stripped data
|
|
||||||
# 1.20 - Added check for source files section (thanks Piquan)
|
|
||||||
# 1.30 - Added prelim Support for K8 style mobis
|
|
||||||
# 1.31 - removed the SRCS section but kept a 0 size entry for it
|
|
||||||
# 1.32 - removes the SRCS section and its entry, now updates metadata 121 if needed
|
|
||||||
# 1.33 - now uses and modifies mobiheader SRCS and CNT
|
|
||||||
# 1.34 - added credit for Kevin Hendricks
|
|
||||||
# 1.35 - fixed bug when more than one compilation (SRCS/CMET) records
|
|
||||||
|
|
||||||
__version__ = '1.35'
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import struct
|
|
||||||
import binascii
|
|
||||||
|
|
||||||
class Unbuffered:
|
|
||||||
def __init__(self, stream):
|
|
||||||
self.stream = stream
|
|
||||||
def write(self, data):
|
|
||||||
self.stream.write(data)
|
|
||||||
self.stream.flush()
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
return getattr(self.stream, attr)
|
|
||||||
|
|
||||||
|
|
||||||
class StripException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SectionStripper:
|
|
||||||
def loadSection(self, section):
|
|
||||||
if (section + 1 == self.num_sections):
|
|
||||||
endoff = len(self.data_file)
|
|
||||||
else:
|
|
||||||
endoff = self.sections[section + 1][0]
|
|
||||||
off = self.sections[section][0]
|
|
||||||
return self.data_file[off:endoff]
|
|
||||||
|
|
||||||
def patch(self, off, new):
|
|
||||||
self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):]
|
|
||||||
|
|
||||||
def strip(self, off, len):
|
|
||||||
self.data_file = self.data_file[:off] + self.data_file[off+len:]
|
|
||||||
|
|
||||||
def patchSection(self, section, new, in_off = 0):
|
|
||||||
if (section + 1 == self.num_sections):
|
|
||||||
endoff = len(self.data_file)
|
|
||||||
else:
|
|
||||||
endoff = self.sections[section + 1][0]
|
|
||||||
off = self.sections[section][0]
|
|
||||||
assert off + in_off + len(new) <= endoff
|
|
||||||
self.patch(off + in_off, new)
|
|
||||||
|
|
||||||
def updateEXTH121(self, srcs_secnum, srcs_cnt, mobiheader):
|
|
||||||
mobi_length, = struct.unpack('>L',mobiheader[0x14:0x18])
|
|
||||||
exth_flag, = struct.unpack('>L', mobiheader[0x80:0x84])
|
|
||||||
exth = 'NONE'
|
|
||||||
try:
|
|
||||||
if exth_flag & 0x40:
|
|
||||||
exth = mobiheader[16 + mobi_length:]
|
|
||||||
if (len(exth) >= 4) and (exth[:4] == 'EXTH'):
|
|
||||||
nitems, = struct.unpack('>I', exth[8:12])
|
|
||||||
pos = 12
|
|
||||||
for i in xrange(nitems):
|
|
||||||
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
|
||||||
# print type, size
|
|
||||||
if type == 121:
|
|
||||||
boundaryptr, =struct.unpack('>L',exth[pos+8: pos + size])
|
|
||||||
if srcs_secnum <= boundaryptr:
|
|
||||||
boundaryptr -= srcs_cnt
|
|
||||||
prefix = mobiheader[0:16 + mobi_length + pos + 8]
|
|
||||||
suffix = mobiheader[16 + mobi_length + pos + 8 + 4:]
|
|
||||||
nval = struct.pack('>L',boundaryptr)
|
|
||||||
mobiheader = prefix + nval + suffix
|
|
||||||
pos += size
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return mobiheader
|
|
||||||
|
|
||||||
def __init__(self, datain):
|
|
||||||
if datain[0x3C:0x3C+8] != 'BOOKMOBI':
|
|
||||||
raise StripException("invalid file format")
|
|
||||||
self.num_sections, = struct.unpack('>H', datain[76:78])
|
|
||||||
|
|
||||||
# get mobiheader and check SRCS section number and count
|
|
||||||
offset0, = struct.unpack_from('>L', datain, 78)
|
|
||||||
offset1, = struct.unpack_from('>L', datain, 86)
|
|
||||||
mobiheader = datain[offset0:offset1]
|
|
||||||
srcs_secnum, srcs_cnt = struct.unpack_from('>2L', mobiheader, 0xe0)
|
|
||||||
if srcs_secnum == 0xffffffff or srcs_cnt == 0:
|
|
||||||
raise StripException("File doesn't contain the sources section.")
|
|
||||||
|
|
||||||
print "Found SRCS section number %d, and count %d" % (srcs_secnum, srcs_cnt)
|
|
||||||
# find its offset and length
|
|
||||||
next = srcs_secnum + srcs_cnt
|
|
||||||
srcs_offset, flgval = struct.unpack_from('>2L', datain, 78+(srcs_secnum*8))
|
|
||||||
next_offset, flgval = struct.unpack_from('>2L', datain, 78+(next*8))
|
|
||||||
srcs_length = next_offset - srcs_offset
|
|
||||||
if datain[srcs_offset:srcs_offset+4] != 'SRCS':
|
|
||||||
raise StripException("SRCS section num does not point to SRCS.")
|
|
||||||
print " beginning at offset %0x and ending at offset %0x" % (srcs_offset, srcs_length)
|
|
||||||
|
|
||||||
# it appears bytes 68-71 always contain (2*num_sections) + 1
|
|
||||||
# this is not documented anyplace at all but it appears to be some sort of next
|
|
||||||
# available unique_id used to identify specific sections in the palm db
|
|
||||||
self.data_file = datain[:68] + struct.pack('>L',((self.num_sections-srcs_cnt)*2+1))
|
|
||||||
self.data_file += datain[72:76]
|
|
||||||
|
|
||||||
# write out the number of sections reduced by srtcs_cnt
|
|
||||||
self.data_file = self.data_file + struct.pack('>H',self.num_sections-srcs_cnt)
|
|
||||||
|
|
||||||
# we are going to remove srcs_cnt SRCS sections so the offset of every entry in the table
|
|
||||||
# up to the srcs secnum must begin 8 bytes earlier per section removed (each table entry is 8 )
|
|
||||||
delta = -8 * srcs_cnt
|
|
||||||
for i in xrange(srcs_secnum):
|
|
||||||
offset, flgval = struct.unpack_from('>2L', datain, 78+(i*8))
|
|
||||||
offset += delta
|
|
||||||
self.data_file += struct.pack('>L',offset) + struct.pack('>L',flgval)
|
|
||||||
|
|
||||||
# for every record after the srcs_cnt SRCS records we must start it
|
|
||||||
# earlier by 8*srcs_cnt + the length of the srcs sections themselves)
|
|
||||||
delta = delta - srcs_length
|
|
||||||
for i in xrange(srcs_secnum+srcs_cnt,self.num_sections):
|
|
||||||
offset, flgval = struct.unpack_from('>2L', datain, 78+(i*8))
|
|
||||||
offset += delta
|
|
||||||
flgval = 2 * (i - srcs_cnt)
|
|
||||||
self.data_file += struct.pack('>L',offset) + struct.pack('>L',flgval)
|
|
||||||
|
|
||||||
# now pad it out to begin right at the first offset
|
|
||||||
# typically this is 2 bytes of nulls
|
|
||||||
first_offset, flgval = struct.unpack_from('>2L', self.data_file, 78)
|
|
||||||
self.data_file += '\0' * (first_offset - len(self.data_file))
|
|
||||||
|
|
||||||
# now finally add on every thing up to the original src_offset
|
|
||||||
self.data_file += datain[offset0: srcs_offset]
|
|
||||||
|
|
||||||
# and everything afterwards
|
|
||||||
self.data_file += datain[srcs_offset+srcs_length:]
|
|
||||||
|
|
||||||
#store away the SRCS section in case the user wants it output
|
|
||||||
self.stripped_data_header = datain[srcs_offset:srcs_offset+16]
|
|
||||||
self.stripped_data = datain[srcs_offset+16:srcs_offset+srcs_length]
|
|
||||||
|
|
||||||
# update the number of sections count
|
|
||||||
self.num_section = self.num_sections - srcs_cnt
|
|
||||||
|
|
||||||
# update the srcs_secnum and srcs_cnt in the mobiheader
|
|
||||||
offset0, flgval0 = struct.unpack_from('>2L', self.data_file, 78)
|
|
||||||
offset1, flgval1 = struct.unpack_from('>2L', self.data_file, 86)
|
|
||||||
mobiheader = self.data_file[offset0:offset1]
|
|
||||||
mobiheader = mobiheader[:0xe0]+ struct.pack('>L', 0xffffffff) + struct.pack('>L', 0) + mobiheader[0xe8:]
|
|
||||||
|
|
||||||
# if K8 mobi, handle metadata 121 in old mobiheader
|
|
||||||
mobiheader = self.updateEXTH121(srcs_secnum, srcs_cnt, mobiheader)
|
|
||||||
self.data_file = self.data_file[0:offset0] + mobiheader + self.data_file[offset1:]
|
|
||||||
print "done"
|
|
||||||
|
|
||||||
def getResult(self):
|
|
||||||
return self.data_file
|
|
||||||
|
|
||||||
def getStrippedData(self):
|
|
||||||
return self.stripped_data
|
|
||||||
|
|
||||||
def getHeader(self):
|
|
||||||
return self.stripped_data_header
|
|
||||||
|
|
||||||
def main(argv=None):
|
|
||||||
infile = argv[0]
|
|
||||||
outfile = argv[1]
|
|
||||||
data_file = file(infile, 'rb').read()
|
|
||||||
try:
|
|
||||||
strippedFile = SectionStripper(data_file)
|
|
||||||
file(outfile, 'wb').write(strippedFile.getResult())
|
|
||||||
print "Header Bytes: " + binascii.b2a_hex(strippedFile.getHeader())
|
|
||||||
if len(argv)==3:
|
|
||||||
file(argv[2], 'wb').write(strippedFile.getStrippedData())
|
|
||||||
except StripException, e:
|
|
||||||
print "Error: %s" % e
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.stdout=Unbuffered(sys.stdout)
|
|
||||||
print ('KindleStrip v%(__version__)s. '
|
|
||||||
'Written 2010-2012 by Paul Durrant and Kevin Hendricks.' % globals())
|
|
||||||
if len(sys.argv)<3 or len(sys.argv)>4:
|
|
||||||
print "Strips the Sources record from Mobipocket ebooks"
|
|
||||||
print "For ebooks generated using KindleGen 1.1 and later that add the source"
|
|
||||||
print "Usage:"
|
|
||||||
print " %s <infile> <outfile> <strippeddatafile>" % sys.argv[0]
|
|
||||||
print "<strippeddatafile> is optional."
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
main(sys.argv[1:])
|
|
||||||
sys.exit(0)
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
rm -rf KindleComicConverter.app/Contents/Resources/*
|
|
||||||
cp -a resources/Scripts resources/description.rtfd resources/droplet.rsrc KindleComicConverter.app/Contents/Resources/
|
|
||||||
cp resources/Info.plist KindleComicConverter.app/Contents/
|
|
||||||
cp resources/comic2ebook.icns KindleComicConverter.app/Contents/Resources/droplet.icns
|
|
||||||
cp kcc/*.py KindleComicConverter.app/Contents/Resources/
|
|
||||||
cp `which unrar` KindleComicConverter.app/Contents/Resources/
|
|
||||||
Reference in New Issue
Block a user