mirror of
https://github.com/ciromattia/kcc
synced 2025-12-22 06:01:49 +00:00
Version 1.2 - comic optimization, mangling, and more coherent codebase
This commit is contained in:
@@ -55,11 +55,11 @@
|
|||||||
<key>name</key>
|
<key>name</key>
|
||||||
<string>ScriptWindowState</string>
|
<string>ScriptWindowState</string>
|
||||||
<key>positionOfDivider</key>
|
<key>positionOfDivider</key>
|
||||||
<real>0.0</real>
|
<real>568</real>
|
||||||
<key>savedFrame</key>
|
<key>savedFrame</key>
|
||||||
<string>444 56 1021 972 0 0 1680 1028 </string>
|
<string>144 338 889 690 0 0 1680 1028 </string>
|
||||||
<key>selectedTabView</key>
|
<key>selectedTabView</key>
|
||||||
<string>event log</string>
|
<string>result</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
Binary file not shown.
81
KindleComicConverter.app/Contents/Resources/cbxarchive.py
Normal file
81
KindleComicConverter.app/Contents/Resources/cbxarchive.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# 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.
|
||||||
|
#
|
||||||
|
__version__ = '1.0'
|
||||||
|
|
||||||
|
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
|
||||||
|
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])
|
||||||
@@ -19,113 +19,48 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
# 1.00 - Initial version
|
# 1.00 - Initial version
|
||||||
# 1.10 - Added support for CBZ/CBR files
|
# 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!
|
||||||
#
|
#
|
||||||
# Todo:
|
# Todo:
|
||||||
# - Add gracefully exit for CBR if no rarfile.py and no unrar
|
# - Add gracefully exit for CBR if no rarfile.py and no unrar
|
||||||
# executable are found
|
# executable are found
|
||||||
# - Improve error reporting
|
# - Improve error reporting
|
||||||
#
|
# - recurse into dirtree for multiple comics
|
||||||
|
|
||||||
__version__ = '1.10'
|
__version__ = '1.20'
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import cbxarchive
|
||||||
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 CBxArchive:
|
|
||||||
def __init__(self, origFileName):
|
|
||||||
self.cbxexts = ['.cbz', '.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
|
|
||||||
cbrFile = rarfile.RarFile(self.origFileName)
|
|
||||||
for f in cbrFile.namelist():
|
|
||||||
if 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()):
|
|
||||||
self.extractCBR()
|
|
||||||
elif ('.cbz' == 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])
|
|
||||||
|
|
||||||
class HTMLbuilder:
|
class HTMLbuilder:
|
||||||
|
|
||||||
def getResult(self):
|
def getResult(self):
|
||||||
if (self.filename[0].startswith('.') or (self.filename[1] != '.png' and self.filename[1] != '.jpg' and self.filename[1] != '.jpeg')):
|
return getImageFileName(self.file)
|
||||||
return None
|
|
||||||
return self.filename
|
|
||||||
|
|
||||||
def __init__(self, dstdir, file):
|
def __init__(self, dstdir, file):
|
||||||
self.filename = os.path.splitext(file)
|
self.file = file
|
||||||
basefilename = self.filename[0]
|
filename = getImageFileName(file)
|
||||||
ext = self.filename[1]
|
if (filename != None):
|
||||||
if (basefilename.startswith('.') or (ext != '.png' and ext != '.jpg' and ext != '.jpeg')):
|
htmlfile = dstdir + '/' + filename[0] + '.html'
|
||||||
return
|
f = open(htmlfile, "w");
|
||||||
htmlfile = dstdir + '/' + basefilename + '.html'
|
f.writelines(["<!DOCTYPE html SYSTEM \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n",
|
||||||
f = open(htmlfile, "w");
|
"<html xmlns=\"http://www.w3.org/1999/xhtml\">\n",
|
||||||
f.writelines(["<!DOCTYPE html SYSTEM \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n",
|
"<head>\n",
|
||||||
"<html xmlns=\"http://www.w3.org/1999/xhtml\">\n",
|
"<title>",filename[0],"</title>\n",
|
||||||
"<head>\n",
|
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n",
|
||||||
"<title>",basefilename,"</title>\n",
|
"</head>\n",
|
||||||
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n",
|
"<body>\n",
|
||||||
"</head>\n",
|
"<div><img src=\"",file,"\" /></div>\n",
|
||||||
"<body>\n",
|
"</body>\n",
|
||||||
"<div><img src=\"",file,"\" /></div>\n",
|
"</html>"
|
||||||
"</body>\n",
|
])
|
||||||
"</html>"
|
f.close()
|
||||||
])
|
return None
|
||||||
f.close()
|
|
||||||
|
|
||||||
class NCXbuilder:
|
class NCXbuilder:
|
||||||
def __init__(self, dstdir, title):
|
def __init__(self, dstdir, title):
|
||||||
@@ -151,7 +86,7 @@ class OPFBuilder:
|
|||||||
width, height = im.size
|
width, height = im.size
|
||||||
imgres = str(width) + "x" + str(height)
|
imgres = str(width) + "x" + str(height)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print "Could not load PIL, falling back on default HD"
|
print "Could not load PIL, falling back on default HD"
|
||||||
imgres = "758x1024"
|
imgres = "758x1024"
|
||||||
f = open(opffile, "w");
|
f = open(opffile, "w");
|
||||||
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
|
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
|
||||||
@@ -181,32 +116,65 @@ class OPFBuilder:
|
|||||||
f.close()
|
f.close()
|
||||||
return
|
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
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.stdout=Unbuffered(sys.stdout)
|
|
||||||
print ('comic2ebook v%(__version__)s. '
|
print ('comic2ebook v%(__version__)s. '
|
||||||
'Written 2012 by Ciro Mattia Gonano.' % globals())
|
'Written 2012 by Ciro Mattia Gonano.' % globals())
|
||||||
if len(sys.argv)<2 or len(sys.argv)>3:
|
if len(sys.argv)<3 or len(sys.argv)>4:
|
||||||
print "Generates HTML, NCX and OPF for a Comic ebook from a bunch of images"
|
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"
|
print "Optimized for creating Mobipockets to be read into Kindle Paperwhite"
|
||||||
print "Usage:"
|
print "Usage:"
|
||||||
print " %s <dir> <title>" % sys.argv[0]
|
print " %s <profile> <dir> <title>" % sys.argv[0]
|
||||||
print " <title> is optional"
|
print " <title> is optional"
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
dir = sys.argv[1]
|
profile = sys.argv[1]
|
||||||
cbx = CBxArchive(dir)
|
dir = sys.argv[2]
|
||||||
|
cbx = cbxarchive.CBxArchive(dir)
|
||||||
if cbx.isCbxFile():
|
if cbx.isCbxFile():
|
||||||
cbx.extract()
|
cbx.extract()
|
||||||
dir = cbx.getPath()
|
dir = cbx.getPath()
|
||||||
if len(sys.argv)==3:
|
if len(sys.argv)==4:
|
||||||
title = sys.argv[2]
|
title = sys.argv[3]
|
||||||
else:
|
else:
|
||||||
title = "comic"
|
title = "comic"
|
||||||
filelist = []
|
filelist = []
|
||||||
|
try:
|
||||||
|
import image
|
||||||
|
print "Splitting double pages..."
|
||||||
|
for file in os.listdir(dir):
|
||||||
|
if (getImageFileName(file) != None):
|
||||||
|
img = image.ComicPage(dir+'/'+file, profile)
|
||||||
|
img.splitPage(dir)
|
||||||
|
for file in os.listdir(dir):
|
||||||
|
if (getImageFileName(file) != None):
|
||||||
|
print "Optimizing " + file + " for " + profile
|
||||||
|
img = image.ComicPage(dir+'/'+file, profile)
|
||||||
|
img.resizeImage()
|
||||||
|
img.frameImage()
|
||||||
|
img.quantizeImage()
|
||||||
|
img.saveToDir(dir)
|
||||||
|
except ImportError:
|
||||||
|
print "Could not load PIL, not optimizing image"
|
||||||
|
|
||||||
for file in os.listdir(dir):
|
for file in os.listdir(dir):
|
||||||
filename = HTMLbuilder(dir,file).getResult()
|
if (getImageFileName(file) != None and isInFilelist(file,filelist) == False):
|
||||||
if (filename != None):
|
filename = HTMLbuilder(dir,file).getResult()
|
||||||
filelist.append(filename)
|
if (filename != None):
|
||||||
|
filelist.append(filename)
|
||||||
NCXbuilder(dir,title)
|
NCXbuilder(dir,title)
|
||||||
OPFBuilder(dir,title,filelist)
|
OPFBuilder(dir,title,filelist)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|||||||
@@ -5,8 +5,18 @@
|
|||||||
|
|
||||||
\f0\fs24 \cf2 \CocoaLigature0 Copyright (c) 2012 Ciro Mattia Gonano <ciromattia@gmail.com>\
|
\f0\fs24 \cf2 \CocoaLigature0 Copyright (c) 2012 Ciro Mattia Gonano <ciromattia@gmail.com>\
|
||||||
\
|
\
|
||||||
This script heavily relies on KindleStrip (C) by Paul Durrant and released in public domain (http://www.mobileread.com/forums/showthread.php?t=96903)\
|
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.\
|
Also, you need to have kindlegen v2.7 (with KF8 support) which is downloadable from Amazon website.\
|
||||||
\
|
\
|
||||||
This script is released under The MIT License (http://opensource.org/licenses/MIT)\
|
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)}
|
||||||
207
KindleComicConverter.app/Contents/Resources/image.py
Executable file
207
KindleComicConverter.app/Contents/Resources/image.py
Executable file
@@ -0,0 +1,207 @@
|
|||||||
|
# Copyright (C) 2010 Alex Yatskov
|
||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
class ImageFlags:
|
||||||
|
Orient = 1 << 0
|
||||||
|
Resize = 1 << 1
|
||||||
|
Frame = 1 << 2
|
||||||
|
Quantize = 1 << 3
|
||||||
|
Stretch = 1 << 4
|
||||||
|
|
||||||
|
|
||||||
|
class KindleData:
|
||||||
|
Palette4 = [
|
||||||
|
0x00, 0x00, 0x00,
|
||||||
|
0x55, 0x55, 0x55,
|
||||||
|
0xaa, 0xaa, 0xaa,
|
||||||
|
0xff, 0xff, 0xff
|
||||||
|
]
|
||||||
|
|
||||||
|
Palette15a = [
|
||||||
|
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,
|
||||||
|
]
|
||||||
|
|
||||||
|
Palette15b = [
|
||||||
|
0x00, 0x00, 0x00,
|
||||||
|
0x11, 0x11, 0x11,
|
||||||
|
0x22, 0x22, 0x22,
|
||||||
|
0x33, 0x33, 0x33,
|
||||||
|
0x44, 0x44, 0x44,
|
||||||
|
0x55, 0x55, 0x55,
|
||||||
|
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': ((600, 800), Palette4),
|
||||||
|
'K2': ((600, 800), Palette15a),
|
||||||
|
'K3': ((600, 800), Palette15a),
|
||||||
|
'K4': ((600, 800), Palette15b),
|
||||||
|
'KHD': ((758, 1024), Palette15b),
|
||||||
|
'KDX': ((824, 1200), Palette15a)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ComicPage:
|
||||||
|
def __init__(self,source,device):
|
||||||
|
try:
|
||||||
|
self.size, self.palette = KindleData.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 quantizeImage(self):
|
||||||
|
colors = len(self.palette) / 3
|
||||||
|
if colors < 256:
|
||||||
|
palette = self.palette + self.palette[:3] * (256 - colors)
|
||||||
|
palImg = Image.new('P', (1, 1))
|
||||||
|
palImg.putpalette(palette)
|
||||||
|
self.image = self.image.quantize(palette=palImg)
|
||||||
|
|
||||||
|
def stretchImage(self):
|
||||||
|
widthDev, heightDev = self.size
|
||||||
|
self.image = self.image.resize((widthDev, heightDev), Image.ANTIALIAS)
|
||||||
|
|
||||||
|
def resizeImage(self):
|
||||||
|
widthDev, heightDev = self.size
|
||||||
|
widthImg, heightImg = self.image.size
|
||||||
|
if widthImg <= widthDev and heightImg <= heightDev:
|
||||||
|
return self.image
|
||||||
|
ratioImg = float(widthImg) / float(heightImg)
|
||||||
|
ratioWidth = float(widthImg) / float(widthDev)
|
||||||
|
ratioHeight = float(heightImg) / float(heightDev)
|
||||||
|
if ratioWidth > ratioHeight:
|
||||||
|
widthImg = widthDev
|
||||||
|
heightImg = int(widthDev / ratioImg)
|
||||||
|
elif ratioWidth < ratioHeight:
|
||||||
|
heightImg = heightDev
|
||||||
|
widthImg = int(heightDev * ratioImg)
|
||||||
|
else:
|
||||||
|
widthImg, heightImg = self.size
|
||||||
|
self.image = self.image.resize((widthImg, heightImg), Image.ANTIALIAS)
|
||||||
|
|
||||||
|
def orientImage(self):
|
||||||
|
widthDev, heightDev = self.size
|
||||||
|
widthImg, heightImg = self.image.size
|
||||||
|
if (widthImg > heightImg) != (widthDev > heightDev):
|
||||||
|
self.image = self.image.rotate(90, Image.BICUBIC, True)
|
||||||
|
|
||||||
|
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 == True):
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
|
||||||
|
# for debug purposes (this file is not meant to be called directly
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
imgfile = sys.argv[1]
|
||||||
|
img = ComicPage(imgfile, "KHD")
|
||||||
|
pages = img.splitPage('temp/',False)
|
||||||
|
if (pages != None):
|
||||||
|
print "%s, %s" % pages
|
||||||
|
sys.exit(0)
|
||||||
|
img.orientImage()
|
||||||
|
img.resizeImage()
|
||||||
|
img.frameImage()
|
||||||
|
img.quantizeImage()
|
||||||
|
img.saveToDir("temp/")
|
||||||
|
sys.exit(0)
|
||||||
35
README.md
35
README.md
@@ -1,6 +1,6 @@
|
|||||||
# KindleComicConverter
|
# KindleComicConverter
|
||||||
|
|
||||||
`KindleComicConverter` is a MacOS X AppleScript droplet to convert image folders to a comic-type Mobipocket ebook to take advantage of the new Panel View mode on Amazon's Kindle.
|
`KindleComicConverter` is a Python script wrapped by a MacOS X AppleScript droplet to convert image folders to a comic-type Mobipocket ebook to take advantage of the new Panel View mode on Amazon's Kindle.
|
||||||
|
|
||||||
## REQUIREMENTS
|
## REQUIREMENTS
|
||||||
- Python (included in MacOS and Linux, follow the [official documentation](http://www.python.org/getit/windows/) to install on Windows)
|
- Python (included in MacOS and Linux, follow the [official documentation](http://www.python.org/getit/windows/) to install on Windows)
|
||||||
@@ -9,36 +9,28 @@
|
|||||||
### for standalone `comic2ebook.py` script:
|
### for standalone `comic2ebook.py` script:
|
||||||
- [unrar](http://www.rarlab.com/download.htm) and [rarfile.py](http://developer.berlios.de/project/showfiles.php?group_id=5373&release_id=18844) for `calibre2ebook.py` automatic CBR extracting.
|
- [unrar](http://www.rarlab.com/download.htm) and [rarfile.py](http://developer.berlios.de/project/showfiles.php?group_id=5373&release_id=18844) for `calibre2ebook.py` automatic CBR extracting.
|
||||||
|
|
||||||
The app and the standalone `comic2ebook.py` script can optionally use the [Python Imaging Library](http://www.pythonware.com/products/pil/) to correctly set the image resolution on OPF file, please refer to official documentation for installing into your system.
|
You are strongly encouraged to get the [Python Imaging Library](http://www.pythonware.com/products/pil/) that, altough optional, provides a bunch of comic optimizations like split double pages, resize to optimal resolution, improve contrast and palette, etc.
|
||||||
|
Please refer to official documentation for installing into your system.
|
||||||
|
|
||||||
## USAGE
|
## USAGE
|
||||||
Drop a folder or a CBZ/CBR file over the droplet, after a while you'll get a comic-type .mobi to sideload on your Kindle.
|
Drop a folder or a CBZ/CBR file over the droplet, after a while you'll get a comic-type .mobi to sideload on your Kindle.
|
||||||
The script takes care of calling `comic2ebook.py`, `kindlegen` and `kindlestrip.py`.
|
The script takes care of calling `comic2ebook.py`, `kindlegen` and `kindlestrip.py`.
|
||||||
|
|
||||||
**WARNING:** at the moment the script does not perform image manipulation. Image optimization and resizing (HD Kindles want 758x1024, non-HD ones 600x800) is up to you.
|
> **WARNING:** at the moment the droplet *ALWAYS* uses the **KHD** profile (*Kindle Paperwhite*).
|
||||||
|
> If you want to specify other profiles, please use the script from command line.
|
||||||
|
|
||||||
### standalone `comic2ebook.py` usage:
|
### standalone `comic2ebook.py` usage:
|
||||||
1. Prepare image folder resizing the images to 758x1024 for HD or 600x800 for non-HD readers, in .png or .jpg formats
|
1. Launch
|
||||||
2. Organize the images into the folders (Use leading 0's to avoid file ordering problems). For example,
|
|
||||||
|
|
||||||
> Legs Weaver 51/
|
```python comic2ebook.py <profile> <directory|file> <title>```
|
||||||
> Legs Weaver 51/001.png
|
|
||||||
> Legs Weaver 51/002.png
|
|
||||||
> etc...
|
|
||||||
|
|
||||||
3. Launch
|
The script takes care of unzipping/unrarring the file if it's an archive, creating a directory of images which should be then filled with a `.opf`, `.ncx`, and many `.html` files.
|
||||||
|
|
||||||
```python comic2ebook.py <directory> <title>```
|
|
||||||
|
|
||||||
The directory should be then filled with a `.opf`, `.ncx`, and many `.html` files.
|
|
||||||
4. Run `Kindlegen` on `content.opf`. Depending on how many images you have, this may take awhile. Once completed, the `.mobi` file should be in the directory.
|
4. Run `Kindlegen` on `content.opf`. Depending on how many images you have, this may take awhile. Once completed, the `.mobi` file should be in the directory.
|
||||||
5. Remove the SRCS record to reduce the `.mobi` filesize in half. You can use [Kindlestrip](http://www.mobileread.com/forums/showthread.php?t=96903).
|
5. Remove the SRCS record to reduce the `.mobi` filesize in half. You can use [Kindlestrip](http://www.mobileread.com/forums/showthread.php?t=96903).
|
||||||
6. Copy the `.mobi` file to your Kindle!
|
6. Copy the `.mobi` file to your Kindle!
|
||||||
|
|
||||||
## CREDITS
|
## CREDITS
|
||||||
This script exists as a cross-platform alternative to `KindleComicParser` by **Dc5e**
|
This script born as a cross-platform alternative to `KindleComicParser` by **Dc5e** (published in [this mobileread forum thread](http://www.mobileread.com/forums/showthread.php?t=192783))
|
||||||
(published in [this mobileread forum thread](http://www.mobileread.com/forums/showthread.php?t=192783))
|
|
||||||
|
|
||||||
|
|
||||||
The app relies and includes the following scripts/binaries:
|
The app relies and includes the following scripts/binaries:
|
||||||
|
|
||||||
@@ -47,6 +39,7 @@ The app relies and includes the following scripts/binaries:
|
|||||||
- the `rarfile.py` script © 2005-2011 **Marko Kreen** <markokr@gmail.com>, released with ISC License
|
- the `rarfile.py` script © 2005-2011 **Marko Kreen** <markokr@gmail.com>, released with ISC License
|
||||||
- the free version `unrar` executable (downloadable from [here](http://www.rarlab.com/rar_add.htm), refer to `LICENSE_unrar.txt` for further details)
|
- the free version `unrar` executable (downloadable from [here](http://www.rarlab.com/rar_add.htm), refer to `LICENSE_unrar.txt` for further details)
|
||||||
- the icon is by **Nikolay Verin** ([http://ncrow.deviantart.com/](http://ncrow.deviantart.com/)) and released under [CC Attribution-NonCommercial-ShareAlike 3.0 Unported](http://creativecommons.org/licenses/by-nc-sa/3.0/) License
|
- the icon is by **Nikolay Verin** ([http://ncrow.deviantart.com/](http://ncrow.deviantart.com/)) and released under [CC Attribution-NonCommercial-ShareAlike 3.0 Unported](http://creativecommons.org/licenses/by-nc-sa/3.0/) License
|
||||||
|
- the `image.py` class from [Mangle](http://foosoft.net/mangle/)
|
||||||
|
|
||||||
Also, you need to have `kindlegen` v2.7 (with KF8 support) which is downloadable from Amazon website
|
Also, you need to have `kindlegen` v2.7 (with KF8 support) which is downloadable from Amazon website
|
||||||
and installed in `/usr/local/bin/`
|
and installed in `/usr/local/bin/`
|
||||||
@@ -56,13 +49,15 @@ and installed in `/usr/local/bin/`
|
|||||||
- 1.00 - Initial version
|
- 1.00 - Initial version
|
||||||
- 1.10 - Added support for CBZ/CBR files in comic2ebook.py
|
- 1.10 - Added support for CBZ/CBR files in comic2ebook.py
|
||||||
- 1.11 - Added support for CBZ/CBR files in KindleComicConverter
|
- 1.11 - Added support for CBZ/CBR files in KindleComicConverter
|
||||||
|
- 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!
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
- bundle a script to manipulate images (to get rid of Mangle/E-nki/whatsoever)
|
|
||||||
|
|
||||||
#### calibre2ebook.py
|
|
||||||
- Add gracefully exit for CBR if no rarfile.py and no unrar executable are found
|
- Add gracefully exit for CBR if no rarfile.py and no unrar executable are found
|
||||||
- Improve error reporting
|
- Improve error reporting
|
||||||
|
- Recurse into dirtree for multiple comics
|
||||||
|
- Create a GUI to allow user control more options
|
||||||
|
|
||||||
## COPYRIGHT
|
## COPYRIGHT
|
||||||
|
|
||||||
|
|||||||
81
cbxarchive.py
Normal file
81
cbxarchive.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# 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.
|
||||||
|
#
|
||||||
|
__version__ = '1.0'
|
||||||
|
|
||||||
|
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
|
||||||
|
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])
|
||||||
176
comic2ebook.py
176
comic2ebook.py
@@ -19,113 +19,48 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
# 1.00 - Initial version
|
# 1.00 - Initial version
|
||||||
# 1.10 - Added support for CBZ/CBR files
|
# 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!
|
||||||
#
|
#
|
||||||
# Todo:
|
# Todo:
|
||||||
# - Add gracefully exit for CBR if no rarfile.py and no unrar
|
# - Add gracefully exit for CBR if no rarfile.py and no unrar
|
||||||
# executable are found
|
# executable are found
|
||||||
# - Improve error reporting
|
# - Improve error reporting
|
||||||
#
|
# - recurse into dirtree for multiple comics
|
||||||
|
|
||||||
__version__ = '1.10'
|
__version__ = '1.20'
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import cbxarchive
|
||||||
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 CBxArchive:
|
|
||||||
def __init__(self, origFileName):
|
|
||||||
self.cbxexts = ['.cbz', '.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
|
|
||||||
cbrFile = rarfile.RarFile(self.origFileName)
|
|
||||||
for f in cbrFile.namelist():
|
|
||||||
if 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()):
|
|
||||||
self.extractCBR()
|
|
||||||
elif ('.cbz' == 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])
|
|
||||||
|
|
||||||
class HTMLbuilder:
|
class HTMLbuilder:
|
||||||
|
|
||||||
def getResult(self):
|
def getResult(self):
|
||||||
if (self.filename[0].startswith('.') or (self.filename[1] != '.png' and self.filename[1] != '.jpg' and self.filename[1] != '.jpeg')):
|
return getImageFileName(self.file)
|
||||||
return None
|
|
||||||
return self.filename
|
|
||||||
|
|
||||||
def __init__(self, dstdir, file):
|
def __init__(self, dstdir, file):
|
||||||
self.filename = os.path.splitext(file)
|
self.file = file
|
||||||
basefilename = self.filename[0]
|
filename = getImageFileName(file)
|
||||||
ext = self.filename[1]
|
if (filename != None):
|
||||||
if (basefilename.startswith('.') or (ext != '.png' and ext != '.jpg' and ext != '.jpeg')):
|
htmlfile = dstdir + '/' + filename[0] + '.html'
|
||||||
return
|
f = open(htmlfile, "w");
|
||||||
htmlfile = dstdir + '/' + basefilename + '.html'
|
f.writelines(["<!DOCTYPE html SYSTEM \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n",
|
||||||
f = open(htmlfile, "w");
|
"<html xmlns=\"http://www.w3.org/1999/xhtml\">\n",
|
||||||
f.writelines(["<!DOCTYPE html SYSTEM \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n",
|
"<head>\n",
|
||||||
"<html xmlns=\"http://www.w3.org/1999/xhtml\">\n",
|
"<title>",filename[0],"</title>\n",
|
||||||
"<head>\n",
|
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n",
|
||||||
"<title>",basefilename,"</title>\n",
|
"</head>\n",
|
||||||
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n",
|
"<body>\n",
|
||||||
"</head>\n",
|
"<div><img src=\"",file,"\" /></div>\n",
|
||||||
"<body>\n",
|
"</body>\n",
|
||||||
"<div><img src=\"",file,"\" /></div>\n",
|
"</html>"
|
||||||
"</body>\n",
|
])
|
||||||
"</html>"
|
f.close()
|
||||||
])
|
return None
|
||||||
f.close()
|
|
||||||
|
|
||||||
class NCXbuilder:
|
class NCXbuilder:
|
||||||
def __init__(self, dstdir, title):
|
def __init__(self, dstdir, title):
|
||||||
@@ -151,7 +86,7 @@ class OPFBuilder:
|
|||||||
width, height = im.size
|
width, height = im.size
|
||||||
imgres = str(width) + "x" + str(height)
|
imgres = str(width) + "x" + str(height)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print "Could not load PIL, falling back on default HD"
|
print "Could not load PIL, falling back on default HD"
|
||||||
imgres = "758x1024"
|
imgres = "758x1024"
|
||||||
f = open(opffile, "w");
|
f = open(opffile, "w");
|
||||||
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
|
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
|
||||||
@@ -181,32 +116,65 @@ class OPFBuilder:
|
|||||||
f.close()
|
f.close()
|
||||||
return
|
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
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.stdout=Unbuffered(sys.stdout)
|
|
||||||
print ('comic2ebook v%(__version__)s. '
|
print ('comic2ebook v%(__version__)s. '
|
||||||
'Written 2012 by Ciro Mattia Gonano.' % globals())
|
'Written 2012 by Ciro Mattia Gonano.' % globals())
|
||||||
if len(sys.argv)<2 or len(sys.argv)>3:
|
if len(sys.argv)<3 or len(sys.argv)>4:
|
||||||
print "Generates HTML, NCX and OPF for a Comic ebook from a bunch of images"
|
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"
|
print "Optimized for creating Mobipockets to be read into Kindle Paperwhite"
|
||||||
print "Usage:"
|
print "Usage:"
|
||||||
print " %s <dir> <title>" % sys.argv[0]
|
print " %s <profile> <dir> <title>" % sys.argv[0]
|
||||||
print " <title> is optional"
|
print " <title> is optional"
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
dir = sys.argv[1]
|
profile = sys.argv[1]
|
||||||
cbx = CBxArchive(dir)
|
dir = sys.argv[2]
|
||||||
|
cbx = cbxarchive.CBxArchive(dir)
|
||||||
if cbx.isCbxFile():
|
if cbx.isCbxFile():
|
||||||
cbx.extract()
|
cbx.extract()
|
||||||
dir = cbx.getPath()
|
dir = cbx.getPath()
|
||||||
if len(sys.argv)==3:
|
if len(sys.argv)==4:
|
||||||
title = sys.argv[2]
|
title = sys.argv[3]
|
||||||
else:
|
else:
|
||||||
title = "comic"
|
title = "comic"
|
||||||
filelist = []
|
filelist = []
|
||||||
|
try:
|
||||||
|
import image
|
||||||
|
print "Splitting double pages..."
|
||||||
|
for file in os.listdir(dir):
|
||||||
|
if (getImageFileName(file) != None):
|
||||||
|
img = image.ComicPage(dir+'/'+file, profile)
|
||||||
|
img.splitPage(dir)
|
||||||
|
for file in os.listdir(dir):
|
||||||
|
if (getImageFileName(file) != None):
|
||||||
|
print "Optimizing " + file + " for " + profile
|
||||||
|
img = image.ComicPage(dir+'/'+file, profile)
|
||||||
|
img.resizeImage()
|
||||||
|
img.frameImage()
|
||||||
|
img.quantizeImage()
|
||||||
|
img.saveToDir(dir)
|
||||||
|
except ImportError:
|
||||||
|
print "Could not load PIL, not optimizing image"
|
||||||
|
|
||||||
for file in os.listdir(dir):
|
for file in os.listdir(dir):
|
||||||
filename = HTMLbuilder(dir,file).getResult()
|
if (getImageFileName(file) != None and isInFilelist(file,filelist) == False):
|
||||||
if (filename != None):
|
filename = HTMLbuilder(dir,file).getResult()
|
||||||
filelist.append(filename)
|
if (filename != None):
|
||||||
|
filelist.append(filename)
|
||||||
NCXbuilder(dir,title)
|
NCXbuilder(dir,title)
|
||||||
OPFBuilder(dir,title,filelist)
|
OPFBuilder(dir,title,filelist)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|||||||
207
image.py
Executable file
207
image.py
Executable file
@@ -0,0 +1,207 @@
|
|||||||
|
# Copyright (C) 2010 Alex Yatskov
|
||||||
|
#
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
class ImageFlags:
|
||||||
|
Orient = 1 << 0
|
||||||
|
Resize = 1 << 1
|
||||||
|
Frame = 1 << 2
|
||||||
|
Quantize = 1 << 3
|
||||||
|
Stretch = 1 << 4
|
||||||
|
|
||||||
|
|
||||||
|
class KindleData:
|
||||||
|
Palette4 = [
|
||||||
|
0x00, 0x00, 0x00,
|
||||||
|
0x55, 0x55, 0x55,
|
||||||
|
0xaa, 0xaa, 0xaa,
|
||||||
|
0xff, 0xff, 0xff
|
||||||
|
]
|
||||||
|
|
||||||
|
Palette15a = [
|
||||||
|
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,
|
||||||
|
]
|
||||||
|
|
||||||
|
Palette15b = [
|
||||||
|
0x00, 0x00, 0x00,
|
||||||
|
0x11, 0x11, 0x11,
|
||||||
|
0x22, 0x22, 0x22,
|
||||||
|
0x33, 0x33, 0x33,
|
||||||
|
0x44, 0x44, 0x44,
|
||||||
|
0x55, 0x55, 0x55,
|
||||||
|
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': ((600, 800), Palette4),
|
||||||
|
'K2': ((600, 800), Palette15a),
|
||||||
|
'K3': ((600, 800), Palette15a),
|
||||||
|
'K4': ((600, 800), Palette15b),
|
||||||
|
'KHD': ((758, 1024), Palette15b),
|
||||||
|
'KDX': ((824, 1200), Palette15a)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ComicPage:
|
||||||
|
def __init__(self,source,device):
|
||||||
|
try:
|
||||||
|
self.size, self.palette = KindleData.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 quantizeImage(self):
|
||||||
|
colors = len(self.palette) / 3
|
||||||
|
if colors < 256:
|
||||||
|
palette = self.palette + self.palette[:3] * (256 - colors)
|
||||||
|
palImg = Image.new('P', (1, 1))
|
||||||
|
palImg.putpalette(palette)
|
||||||
|
self.image = self.image.quantize(palette=palImg)
|
||||||
|
|
||||||
|
def stretchImage(self):
|
||||||
|
widthDev, heightDev = self.size
|
||||||
|
self.image = self.image.resize((widthDev, heightDev), Image.ANTIALIAS)
|
||||||
|
|
||||||
|
def resizeImage(self):
|
||||||
|
widthDev, heightDev = self.size
|
||||||
|
widthImg, heightImg = self.image.size
|
||||||
|
if widthImg <= widthDev and heightImg <= heightDev:
|
||||||
|
return self.image
|
||||||
|
ratioImg = float(widthImg) / float(heightImg)
|
||||||
|
ratioWidth = float(widthImg) / float(widthDev)
|
||||||
|
ratioHeight = float(heightImg) / float(heightDev)
|
||||||
|
if ratioWidth > ratioHeight:
|
||||||
|
widthImg = widthDev
|
||||||
|
heightImg = int(widthDev / ratioImg)
|
||||||
|
elif ratioWidth < ratioHeight:
|
||||||
|
heightImg = heightDev
|
||||||
|
widthImg = int(heightDev * ratioImg)
|
||||||
|
else:
|
||||||
|
widthImg, heightImg = self.size
|
||||||
|
self.image = self.image.resize((widthImg, heightImg), Image.ANTIALIAS)
|
||||||
|
|
||||||
|
def orientImage(self):
|
||||||
|
widthDev, heightDev = self.size
|
||||||
|
widthImg, heightImg = self.image.size
|
||||||
|
if (widthImg > heightImg) != (widthDev > heightDev):
|
||||||
|
self.image = self.image.rotate(90, Image.BICUBIC, True)
|
||||||
|
|
||||||
|
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 == True):
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
|
||||||
|
# for debug purposes (this file is not meant to be called directly
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
imgfile = sys.argv[1]
|
||||||
|
img = ComicPage(imgfile, "KHD")
|
||||||
|
pages = img.splitPage('temp/',False)
|
||||||
|
if (pages != None):
|
||||||
|
print "%s, %s" % pages
|
||||||
|
sys.exit(0)
|
||||||
|
img.orientImage()
|
||||||
|
img.resizeImage()
|
||||||
|
img.frameImage()
|
||||||
|
img.quantizeImage()
|
||||||
|
img.saveToDir("temp/")
|
||||||
|
sys.exit(0)
|
||||||
Reference in New Issue
Block a user