mirror of
https://github.com/ciromattia/kcc
synced 2025-12-13 01:36:27 +00:00
Version 1.2 - comic optimization, mangling, and more coherent codebase
This commit is contained in:
@@ -55,11 +55,11 @@
|
||||
<key>name</key>
|
||||
<string>ScriptWindowState</string>
|
||||
<key>positionOfDivider</key>
|
||||
<real>0.0</real>
|
||||
<real>568</real>
|
||||
<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>
|
||||
<string>event log</string>
|
||||
<string>result</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</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
|
||||
# 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!
|
||||
#
|
||||
# Todo:
|
||||
# - Add gracefully exit for CBR if no rarfile.py and no unrar
|
||||
# executable are found
|
||||
# - Improve error reporting
|
||||
#
|
||||
# - recurse into dirtree for multiple comics
|
||||
|
||||
__version__ = '1.10'
|
||||
__version__ = '1.20'
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
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])
|
||||
import cbxarchive
|
||||
|
||||
class HTMLbuilder:
|
||||
|
||||
def getResult(self):
|
||||
if (self.filename[0].startswith('.') or (self.filename[1] != '.png' and self.filename[1] != '.jpg' and self.filename[1] != '.jpeg')):
|
||||
return None
|
||||
return self.filename
|
||||
return getImageFileName(self.file)
|
||||
|
||||
def __init__(self, dstdir, file):
|
||||
self.filename = os.path.splitext(file)
|
||||
basefilename = self.filename[0]
|
||||
ext = self.filename[1]
|
||||
if (basefilename.startswith('.') or (ext != '.png' and ext != '.jpg' and ext != '.jpeg')):
|
||||
return
|
||||
htmlfile = dstdir + '/' + basefilename + '.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>",basefilename,"</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()
|
||||
self.file = file
|
||||
filename = getImageFileName(file)
|
||||
if (filename != None):
|
||||
htmlfile = dstdir + '/' + 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 None
|
||||
|
||||
class NCXbuilder:
|
||||
def __init__(self, dstdir, title):
|
||||
@@ -151,7 +86,7 @@ class OPFBuilder:
|
||||
width, height = im.size
|
||||
imgres = str(width) + "x" + str(height)
|
||||
except ImportError:
|
||||
print "Could not load PIL, falling back on default HD"
|
||||
print "Could not load PIL, falling back on default HD"
|
||||
imgres = "758x1024"
|
||||
f = open(opffile, "w");
|
||||
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
|
||||
@@ -181,32 +116,65 @@ class OPFBuilder:
|
||||
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
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
print ('comic2ebook v%(__version__)s. '
|
||||
'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 "Optimized for creating Mobipockets to be read into Kindle Paperwhite"
|
||||
print "Usage:"
|
||||
print " %s <dir> <title>" % sys.argv[0]
|
||||
print " %s <profile> <dir> <title>" % sys.argv[0]
|
||||
print " <title> is optional"
|
||||
sys.exit(1)
|
||||
else:
|
||||
dir = sys.argv[1]
|
||||
cbx = CBxArchive(dir)
|
||||
profile = sys.argv[1]
|
||||
dir = sys.argv[2]
|
||||
cbx = cbxarchive.CBxArchive(dir)
|
||||
if cbx.isCbxFile():
|
||||
cbx.extract()
|
||||
dir = cbx.getPath()
|
||||
if len(sys.argv)==3:
|
||||
title = sys.argv[2]
|
||||
if len(sys.argv)==4:
|
||||
title = sys.argv[3]
|
||||
else:
|
||||
title = "comic"
|
||||
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):
|
||||
filename = HTMLbuilder(dir,file).getResult()
|
||||
if (filename != None):
|
||||
filelist.append(filename)
|
||||
if (getImageFileName(file) != None and isInFilelist(file,filelist) == False):
|
||||
filename = HTMLbuilder(dir,file).getResult()
|
||||
if (filename != None):
|
||||
filelist.append(filename)
|
||||
NCXbuilder(dir,title)
|
||||
OPFBuilder(dir,title,filelist)
|
||||
sys.exit(0)
|
||||
|
||||
@@ -5,8 +5,18 @@
|
||||
|
||||
\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.\
|
||||
\
|
||||
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` 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
|
||||
- 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:
|
||||
- [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
|
||||
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`.
|
||||
|
||||
**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:
|
||||
1. Prepare image folder resizing the images to 758x1024 for HD or 600x800 for non-HD readers, in .png or .jpg formats
|
||||
2. Organize the images into the folders (Use leading 0's to avoid file ordering problems). For example,
|
||||
1. Launch
|
||||
|
||||
> Legs Weaver 51/
|
||||
> Legs Weaver 51/001.png
|
||||
> Legs Weaver 51/002.png
|
||||
> etc...
|
||||
```python comic2ebook.py <profile> <directory|file> <title>```
|
||||
|
||||
3. Launch
|
||||
|
||||
```python comic2ebook.py <directory> <title>```
|
||||
|
||||
The directory should be then filled with a `.opf`, `.ncx`, and many `.html` files.
|
||||
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.
|
||||
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).
|
||||
6. Copy the `.mobi` file to your Kindle!
|
||||
|
||||
## CREDITS
|
||||
This script exists as a cross-platform alternative to `KindleComicParser` by **Dc5e**
|
||||
(published in [this mobileread forum thread](http://www.mobileread.com/forums/showthread.php?t=192783))
|
||||
|
||||
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))
|
||||
|
||||
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 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 `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
|
||||
and installed in `/usr/local/bin/`
|
||||
@@ -56,13 +49,15 @@ and installed in `/usr/local/bin/`
|
||||
- 1.00 - Initial version
|
||||
- 1.10 - Added support for CBZ/CBR files in comic2ebook.py
|
||||
- 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
|
||||
- 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
|
||||
- Improve error reporting
|
||||
- Recurse into dirtree for multiple comics
|
||||
- Create a GUI to allow user control more options
|
||||
|
||||
## 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
|
||||
# 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!
|
||||
#
|
||||
# Todo:
|
||||
# - Add gracefully exit for CBR if no rarfile.py and no unrar
|
||||
# executable are found
|
||||
# - Improve error reporting
|
||||
#
|
||||
# - recurse into dirtree for multiple comics
|
||||
|
||||
__version__ = '1.10'
|
||||
__version__ = '1.20'
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
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])
|
||||
import cbxarchive
|
||||
|
||||
class HTMLbuilder:
|
||||
|
||||
def getResult(self):
|
||||
if (self.filename[0].startswith('.') or (self.filename[1] != '.png' and self.filename[1] != '.jpg' and self.filename[1] != '.jpeg')):
|
||||
return None
|
||||
return self.filename
|
||||
return getImageFileName(self.file)
|
||||
|
||||
def __init__(self, dstdir, file):
|
||||
self.filename = os.path.splitext(file)
|
||||
basefilename = self.filename[0]
|
||||
ext = self.filename[1]
|
||||
if (basefilename.startswith('.') or (ext != '.png' and ext != '.jpg' and ext != '.jpeg')):
|
||||
return
|
||||
htmlfile = dstdir + '/' + basefilename + '.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>",basefilename,"</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()
|
||||
self.file = file
|
||||
filename = getImageFileName(file)
|
||||
if (filename != None):
|
||||
htmlfile = dstdir + '/' + 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 None
|
||||
|
||||
class NCXbuilder:
|
||||
def __init__(self, dstdir, title):
|
||||
@@ -151,7 +86,7 @@ class OPFBuilder:
|
||||
width, height = im.size
|
||||
imgres = str(width) + "x" + str(height)
|
||||
except ImportError:
|
||||
print "Could not load PIL, falling back on default HD"
|
||||
print "Could not load PIL, falling back on default HD"
|
||||
imgres = "758x1024"
|
||||
f = open(opffile, "w");
|
||||
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
|
||||
@@ -181,32 +116,65 @@ class OPFBuilder:
|
||||
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
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
print ('comic2ebook v%(__version__)s. '
|
||||
'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 "Optimized for creating Mobipockets to be read into Kindle Paperwhite"
|
||||
print "Usage:"
|
||||
print " %s <dir> <title>" % sys.argv[0]
|
||||
print " %s <profile> <dir> <title>" % sys.argv[0]
|
||||
print " <title> is optional"
|
||||
sys.exit(1)
|
||||
else:
|
||||
dir = sys.argv[1]
|
||||
cbx = CBxArchive(dir)
|
||||
profile = sys.argv[1]
|
||||
dir = sys.argv[2]
|
||||
cbx = cbxarchive.CBxArchive(dir)
|
||||
if cbx.isCbxFile():
|
||||
cbx.extract()
|
||||
dir = cbx.getPath()
|
||||
if len(sys.argv)==3:
|
||||
title = sys.argv[2]
|
||||
if len(sys.argv)==4:
|
||||
title = sys.argv[3]
|
||||
else:
|
||||
title = "comic"
|
||||
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):
|
||||
filename = HTMLbuilder(dir,file).getResult()
|
||||
if (filename != None):
|
||||
filelist.append(filename)
|
||||
if (getImageFileName(file) != None and isInFilelist(file,filelist) == False):
|
||||
filename = HTMLbuilder(dir,file).getResult()
|
||||
if (filename != None):
|
||||
filelist.append(filename)
|
||||
NCXbuilder(dir,title)
|
||||
OPFBuilder(dir,title,filelist)
|
||||
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