diff --git a/KindleComicConverter.app/Contents/Resources/comic2ebook.py b/KindleComicConverter.app/Contents/Resources/comic2ebook.py
index 05f50db..0220d67 100755
--- a/KindleComicConverter.app/Contents/Resources/comic2ebook.py
+++ b/KindleComicConverter.app/Contents/Resources/comic2ebook.py
@@ -26,6 +26,9 @@
# WARNING: PIL is required for all image mangling!
# 1.30 - Fixed an issue in OPF generation for device resolution
# Reworked options system (call with -h option to get the inline help)
+# 1.40 - Added some options for controlling image optimization
+# Further optimization (ImageOps, page numbering cut, autocontrast)
+# 1.41 - Fixed a serious bug on resizing when img ratio was bigger than device one
#
# Todo:
# - Add gracefully exit for CBR if no rarfile.py and no unrar
@@ -48,9 +51,9 @@ class HTMLbuilder:
def __init__(self, dstdir, file):
self.file = file
filename = getImageFileName(file)
- if (filename != None):
+ if filename is not None:
htmlfile = dstdir + '/' + filename[0] + '.html'
- f = open(htmlfile, "w");
+ f = open(htmlfile, "w")
f.writelines(["\n",
"\n",
"
\n",
@@ -63,12 +66,12 @@ class HTMLbuilder:
""
])
f.close()
- return None
+ return
class NCXbuilder:
def __init__(self, dstdir, title):
ncxfile = dstdir + '/content.ncx'
- f = open(ncxfile, "w");
+ f = open(ncxfile, "w")
f.writelines(["\n",
"\n",
"\n",
@@ -83,9 +86,9 @@ class OPFBuilder:
def __init__(self, profile, dstdir, title, filelist):
opffile = dstdir + '/content.opf'
# read the first file resolution
- deviceres, palette = image.ProfileData.Profiles[profile]
+ profilelabel, deviceres, palette = image.ProfileData.Profiles[profile]
imgres = str(deviceres[0]) + "x" + str(deviceres[1])
- f = open(opffile, "w");
+ f = open(opffile, "w")
f.writelines(["\n",
"\n",
"\n",
@@ -101,10 +104,10 @@ class OPFBuilder:
for filename in filelist:
f.write("- \n")
for filename in filelist:
- if ('.png' == filename[1]):
- mt = 'image/png';
+ if '.png' == filename[1]:
+ mt = 'image/png'
else:
- mt = 'image/jpeg';
+ mt = 'image/jpeg'
f.write("
\n")
f.write("\n\n")
for filename in filelist:
@@ -115,7 +118,7 @@ class OPFBuilder:
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')):
+ if filename[0].startswith('.') or (filename[1].lower() != '.png' and filename[1].lower() != '.jpg' and filename[1].lower() != '.jpeg'):
return None
return filename
@@ -134,9 +137,6 @@ def Copyright():
def Usage():
print "Generates HTML, NCX and OPF for a Comic ebook from a bunch of images"
print "Optimized for creating Mobipockets to be read into Kindle Paperwhite"
- #print "Usage:"
- #print " %s " % sys.argv[0]
- #print " is optional"
parser.print_help()
def main(argv=None):
@@ -144,18 +144,28 @@ def main(argv=None):
usage = "Usage: %prog [options] comic_file|comic_folder"
parser = OptionParser(usage=usage, version=__version__)
parser.add_option("-p", "--profile", action="store", dest="profile", default="KHD",
- help="Device profile (choose one among K1, K2, K3, K4, KHD [default])")
- parser.add_option("-t", "--title", action="store", dest="title", default="comic",
- help="Comic title")
+ help="Device profile (choose one among K1, K2, K3, K4, KDX, KDXG or KHD) [default=KHD]")
+ parser.add_option("-t", "--title", action="store", dest="title", default="defaulttitle",
+ help="Comic title [default=filename]")
parser.add_option("-m", "--manga-style", action="store_true", dest="righttoleft", default=False,
- help="Split pages 'manga style' (right-to-left reading)")
+ help="Split pages 'manga style' (right-to-left reading) [default=False]")
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False,
+ help="Verbose output [default=False]")
+ parser.add_option("-i", "--image-processing", action="store_false", dest="imgproc", default=True,
+ help="Apply image preprocessing (page splitting and optimizations) [default=True]")
+ parser.add_option("--upscale-images", action="store_true", dest="upscale", default=False,
+ help="Resize images smaller than device's resolution [default=False]")
+ parser.add_option("--stretch-images", action="store_true", dest="stretch", default=False,
+ help="Stretch images to device's resolution [default=False]")
+ parser.add_option("--cut-page-numbers", action="store_false", dest="cutpagenumbers", default=True,
+ help="Try to cut page numbering on images [default=True]")
options, args = parser.parse_args(argv)
if len(args) != 1:
parser.print_help()
return
dir = args[0]
fname = os.path.splitext(dir)
- if (fname[1].lower() == '.pdf'):
+ if fname[1].lower() == '.pdf':
pdf = pdfjpgextract.PdfJpgExtract(dir)
pdf.extract()
dir = pdf.getPath()
@@ -164,38 +174,59 @@ def main(argv=None):
if cbx.isCbxFile():
cbx.extract()
dir = cbx.getPath()
+ else:
+ try:
+ import shutil
+ shutil.copytree(dir, dir + "_orig")
+ #dir = dir + "_orig"
+ except OSError as exc:
+ raise
filelist = []
- try:
- print "Splitting double pages..."
- for file in os.listdir(dir):
- if (getImageFileName(file) != None):
- img = image.ComicPage(dir+'/'+file, options.profile)
- img.splitPage(dir, options.righttoleft)
- for file in os.listdir(dir):
- if (getImageFileName(file) != None):
- print "Optimizing " + file + " for " + options.profile
- img = image.ComicPage(dir+'/'+file, options.profile)
- img.resizeImage()
- #img.frameImage()
- img.quantizeImage()
- img.saveToDir(dir)
- except ImportError:
- print "Could not load PIL, not optimizing image"
+ if options.imgproc:
+ print "Processing images..."
+ try:
+ if options.verbose:
+ print "Splitting double pages..."
+ for file in os.listdir(dir):
+ if getImageFileName(file) is not None:
+ print ".",
+ img = image.ComicPage(dir+'/'+file, options.profile)
+ img.splitPage(dir, options.righttoleft)
+ for file in os.listdir(dir):
+ if getImageFileName(file) is not None:
+ if options.verbose:
+ print "Optimizing " + file + " for " + options.profile
+ else:
+ print ".",
+ img = image.ComicPage(dir+'/'+file, options.profile)
+ img.optimizeImage()
+ img.cropWhiteSpace(10.0)
+ if options.cutpagenumbers:
+ img.cutPageNumber()
+ img.resizeImage(options.upscale,options.stretch)
+ img.quantizeImage()
+ img.saveToDir(dir)
+ except ImportError:
+ print "Could not load PIL, not optimizing image"
+ print "Creating ePub structure..."
for file in os.listdir(dir):
- if (getImageFileName(file) != None and isInFilelist(file,filelist) == False):
+ if getImageFileName(file) is not None and isInFilelist(file,filelist) == False:
# put credits at the end
if "credits" in file.lower():
os.rename(dir+'/'+file, dir+'/ZZZ999_'+file)
file = 'ZZZ999_'+file
filename = HTMLbuilder(dir,file).getResult()
- if (filename != None):
+ if filename is not None:
filelist.append(filename)
+ if options.title == 'defaulttitle':
+ options.title = os.path.basename(dir)
NCXbuilder(dir,options.title)
# ensure we're sorting files alphabetically
- filelist = sorted(filelist, key=lambda name: name[0])
+ filelist = sorted(filelist, key=lambda name: name[0].lower())
OPFBuilder(options.profile,dir,options.title,filelist)
+
if __name__ == "__main__":
Copyright()
main(sys.argv[1:])
diff --git a/KindleComicConverter.app/Contents/Resources/image.py b/KindleComicConverter.app/Contents/Resources/image.py
index d072a78..c7406bd 100755
--- a/KindleComicConverter.app/Contents/Resources/image.py
+++ b/KindleComicConverter.app/Contents/Resources/image.py
@@ -1,4 +1,6 @@
# Copyright (C) 2010 Alex Yatskov
+# Copyright (C) 2011 Stanislav (proDOOMman) Kosolapov
+# Copyright (C) 2012-2013 Ciro Mattia Gonano
#
# 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
@@ -14,7 +16,7 @@
# along with this program. If not, see .
import os
-from PIL import Image, ImageDraw
+from PIL import Image, ImageOps, ImageDraw, ImageStat
class ImageFlags:
Orient = 1 << 0
@@ -32,7 +34,7 @@ class ProfileData:
0xff, 0xff, 0xff
]
- Palette15a = [
+ Palette15 = [
0x00, 0x00, 0x00,
0x11, 0x11, 0x11,
0x22, 0x22, 0x22,
@@ -50,13 +52,14 @@ class ProfileData:
0xff, 0xff, 0xff,
]
- Palette15b = [
+ Palette16 = [
0x00, 0x00, 0x00,
0x11, 0x11, 0x11,
0x22, 0x22, 0x22,
0x33, 0x33, 0x33,
0x44, 0x44, 0x44,
0x55, 0x55, 0x55,
+ 0x66, 0x66, 0x66,
0x77, 0x77, 0x77,
0x88, 0x88, 0x88,
0x99, 0x99, 0x99,
@@ -69,18 +72,19 @@ class ProfileData:
]
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)
+ 'K1': ("Kindle", (600, 800), Palette4),
+ 'K2': ("Kindle 2", (600, 800), Palette15),
+ 'K3': ("Kindle 3/Keyboard", (600, 800), Palette16),
+ 'K4': ("Kindle 4/NT/Touch", (600, 800), Palette16),
+ 'KHD': ("Kindle Paperwhite", (758, 1024), Palette16),
+ 'KDX': ("Kindle DX", (824, 1200), Palette15),
+ 'KDXG': ("Kindle DXG", (824, 1200), Palette16)
}
class ComicPage:
def __init__(self,source,device):
try:
- self.size, self.palette = ProfileData.Profiles[device]
+ self.profile_label, self.size, self.palette = ProfileData.Profiles[device]
except KeyError:
raise RuntimeError('Unexpected output device %s' % device)
try:
@@ -92,56 +96,57 @@ class ComicPage:
def saveToDir(self,targetdir):
filename = os.path.basename(self.origFileName)
- print "Saving to " + targetdir + '/' + filename
+ #print "Saving to " + targetdir + '/' + filename
try:
self.image = self.image.convert('L') # convert to grayscale
self.image.save(targetdir + '/' + filename,"JPEG")
except IOError as e:
raise RuntimeError('Cannot write image in directory %s: %s' %(targetdir,e))
+ def optimizeImage(self):
+ self.image = ImageOps.autocontrast(self.image)
+
def quantizeImage(self):
colors = len(self.palette) / 3
if colors < 256:
- palette = self.palette + self.palette[:3] * (256 - colors)
+ self.palette = self.palette + self.palette[:3] * (256 - colors)
palImg = Image.new('P', (1, 1))
- palImg.putpalette(palette)
+ palImg.putpalette(self.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,upscale=False, stretch=False):
+ method = Image.ANTIALIAS
+ if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]:
+ if not upscale:
+ return self.image
+ else:
+ method = Image.NEAREST
- def resizeImage(self):
- widthDev, heightDev = self.size
- widthImg, heightImg = self.image.size
- if widthImg <= widthDev and heightImg <= heightDev:
+ if stretch: # if stretching call directly resize() without other considerations.
+ self.image = self.image.resize(self.size,method)
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)
+ ratioDev = float(self.size[0]) / float(self.size[1])
+ if (float(self.image.size[0]) / float(self.image.size[1])) < ratioDev:
+ diff = int(self.image.size[1] * ratioDev) - self.image.size[0]
+ newImage = Image.new('RGB', (self.image.size[0] + diff, self.image.size[1]), (255,255,255))
+ newImage.paste(self.image, (diff / 2, 0, diff / 2 + self.image.size[0], self.image.size[1]))
+ self.image = newImage
+ elif (float(self.image.size[0]) / float(self.image.size[1])) > ratioDev:
+ diff = int(self.image.size[0] / ratioDev) - self.image.size[1]
+ newImage = Image.new('RGB', (self.image.size[0], self.image.size[1] + diff), (255,255,255))
+ newImage.paste(self.image, (0, diff / 2, self.image.size[0], diff / 2 + self.image.size[1]))
+ self.image = newImage
+ self.image = ImageOps.fit(self.image, self.size, method = method, centering = (0.5,0.5))
+ return self.image
def splitPage(self, targetdir, righttoleft=False):
width, height = self.image.size
dstwidth, dstheight = self.size
- print "Image is %d x %d" % (width,height)
+ #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):
+ if width > height:
# source is landscape, so split by the width
leftbox = (0, 0, width/2, height)
rightbox = (width/2, 0, width, height)
@@ -153,7 +158,7 @@ class ComicPage:
fileone = targetdir + '/' + filename[0] + '-1' + filename[1]
filetwo = targetdir + '/' + filename[0] + '-2' + filename[1]
try:
- if (righttoleft == True):
+ if righttoleft:
pageone = self.image.crop(rightbox)
pagetwo = self.image.crop(leftbox)
else:
@@ -164,7 +169,7 @@ class ComicPage:
os.remove(self.origFileName)
except IOError as e:
raise RuntimeError('Cannot write image in directory %s: %s' %(targetdir,e))
- return (fileone,filetwo)
+ return fileone,filetwo
return None
def frameImage(self):
@@ -190,18 +195,126 @@ class ComicPage:
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)
+
+ def cutPageNumber(self):
+ widthImg, heightImg = self.image.size
+ delta = 2
+ diff = delta
+ fixedThreshold = 5
+ if ImageStat.Stat(self.image).var[0] < 2*fixedThreshold:
+ return self.image
+ while ImageStat.Stat(self.image.crop((0,heightImg-diff,widthImg,heightImg))).var[0] < fixedThreshold\
+ and diff < heightImg:
+ diff += delta
+ diff -= delta
+ pageNumberCut1 = diff
+ if diff 0\
+ and diff < heightImg/4:
+ oldStat=ImageStat.Stat(self.image.crop((0,heightImg-diff,widthImg,heightImg))).var[0]
+ diff += delta
+ diff -= delta
+ pageNumberCut2 = diff
+ diff += delta
+ oldStat=ImageStat.Stat(self.image.crop((0,heightImg-diff,widthImg,heightImg-pageNumberCut2))).var[0]
+ while ImageStat.Stat(self.image.crop((0,heightImg-diff,widthImg,heightImg-pageNumberCut2))).var[0] < fixedThreshold+oldStat\
+ and diff < heightImg/4:
+ diff += delta
+ diff -= delta
+ pageNumberCut3 = diff
+ delta = 5
+ diff = delta
+ while ImageStat.Stat(self.image.crop((0,heightImg-pageNumberCut2,diff,heightImg))).var[0] < fixedThreshold and diff < widthImg:
+ diff += delta
+ diff -= delta
+ pageNumberX1 = diff
+ diff = delta
+ while ImageStat.Stat(self.image.crop((widthImg-diff,heightImg-pageNumberCut2,widthImg,heightImg))).var[0] < fixedThreshold and diff < widthImg:
+ diff += delta
+ diff -= delta
+ pageNumberX2=widthImg-diff
+
+ if pageNumberCut3-pageNumberCut1 > 2*delta\
+ and float(pageNumberX2-pageNumberX1)/float(pageNumberCut2-pageNumberCut1) <= 9.0\
+ and ImageStat.Stat(self.image.crop((0,heightImg-pageNumberCut3,widthImg,heightImg))).var[0] / ImageStat.Stat(self.image).var[0] < 0.1\
+ and pageNumberCut3 < heightImg/4-delta:
+ diff=pageNumberCut3
+ else:
+ diff=pageNumberCut1
+ self.image = self.image.crop((0,0,widthImg,heightImg-diff))
+ return self.image
+
+ def cropWhiteSpace(self, threshold):
+ widthImg, heightImg = self.image.size
+ delta = 10
+ diff = delta
+ # top
+ while ImageStat.Stat(self.image.crop((0,0,widthImg,diff))).var[0] < threshold and diff < heightImg:
+ diff += delta
+ diff -= delta
+ # print "Top crop: %s"%diff
+ self.image = self.image.crop((0,diff,widthImg,heightImg))
+ widthImg, heightImg = self.image.size
+ diff = delta
+ # left
+ while ImageStat.Stat(self.image.crop((0,0,diff,heightImg))).var[0] < threshold and diff < widthImg:
+ diff += delta
+ diff -= delta
+ # print "Left crop: %s"%diff
+ self.image = self.image.crop((diff,0,widthImg,heightImg))
+ widthImg, heightImg = self.image.size
+ diff = delta
+ # down
+ while ImageStat.Stat(self.image.crop((0,heightImg-diff,widthImg,heightImg))).var[0] < threshold\
+ and diff < heightImg:
+ diff += delta
+ diff -= delta
+ # print "Down crop: %s"%diff
+ self.image = self.image.crop((0,0,widthImg,heightImg-diff))
+ widthImg, heightImg = self.image.size
+ diff = delta
+ # right
+ while ImageStat.Stat(self.image.crop((widthImg-diff,0,widthImg,heightImg))).var[0] < threshold\
+ and diff < widthImg:
+ diff += delta
+ diff -= delta
+ # print "Right crop: %s"%diff
+ self.image = self.image.crop((0,0 ,widthImg-diff,heightImg))
+ # print "New size: %sx%s"%(self.image.size[0],self.image.size[1])
+ return self.image
+
+ def addProgressbar(self, file_number, files_totalnumber, size, howoften):
+ if file_number//howoften!=float(file_number)/howoften:
+ return self.image
+ white = (255,255,255)
+ black = (0,0,0)
+ widthDev, heightDev = size
+ widthImg, heightImg = self.image.size
+ pastePt = (
+ max(0, (widthDev - widthImg) / 2),
+ max(0, (heightDev - heightImg) / 2)
+ )
+ imageBg = Image.new('RGB',size,white)
+ imageBg.paste(self.image, pastePt)
+ self.image = imageBg
+ widthImg, heightImg = self.image.size
+ draw = ImageDraw.Draw(self.image)
+ #Black rectangle
+ draw.rectangle([(0,heightImg-3), (widthImg,heightImg)], outline=black, fill=black)
+ #White rectangle
+ draw.rectangle([(widthImg*file_number/files_totalnumber,heightImg-3), (widthImg-1,heightImg)], outline=black, fill=white)
+ #Making notches
+ for i in range(1,10):
+ if i <= (10*file_number/files_totalnumber):
+ notch_colour=white #White
+ else:
+ notch_colour=black #Black
+ draw.line([(widthImg*float(i)/10,heightImg-3), (widthImg*float(i)/10,heightImg)],fill=notch_colour)
+ #The 50%
+ if i==5:
+ draw.rectangle([(widthImg/2-1,heightImg-5), (widthImg/2+1,heightImg)],outline=black,fill=notch_colour)
+ return self.image
+