diff options
-rw-r--r-- | README.md | 14 | ||||
-rwxr-xr-x | kcc/comic2ebook.py | 46 | ||||
-rw-r--r-- | kcc/gui.py | 29 | ||||
-rwxr-xr-x | kcc/image.py | 184 |
4 files changed, 199 insertions, 74 deletions
diff --git a/README.md b/README.md index 949f2fc..a173db6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,14 @@ # KindleComicConverter -`KindleComicConverter` is a Python app which aim is 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 app which aim is to convert comic files or folders to a comic-type (Mobipocket) ebook to take advantage of the new Panel View mode on Amazon's Kindle. + +## INPUT FORMATS +`kcc` can understand and convert, at the moment, the following file types: +- CBZ, ZIP +- CBR, RAR +- flat folders +- PDF *(extracting only contained JPG images)* +For now the script does not understand folder depth, so it will work on flat folders/archives only. ## REQUIREMENTS - `kindlegen` in /usr/local/bin/ @@ -47,7 +55,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/) + - the `image.py` class from **Alex Yatskov**'s [Mangle](http://foosoft.net/mangle/) with subsequent [proDOOMman](https://github.com/proDOOMman/Mangle)'s and [Birua](https://github.com/Birua/Mangle)'s patches Also, you need to have `kindlegen` v2.7 (with KF8 support) which is downloadable from Amazon website and installed in `/usr/local/bin/` @@ -72,4 +80,4 @@ and installed in `/usr/local/bin/` ## COPYRIGHT -Copyright (c) 2012-2013 Ciro Mattia Gonano. See LICENSE.txt for further details. \ No newline at end of file +Copyright (c) 2012-2013 Ciro Mattia Gonano. See LICENSE.txt for further details. diff --git a/kcc/comic2ebook.py b/kcc/comic2ebook.py index 05f50db..8d23a68 100755 --- a/kcc/comic2ebook.py +++ b/kcc/comic2ebook.py @@ -48,9 +48,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(["<!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", @@ -63,12 +63,12 @@ class HTMLbuilder: "</html>" ]) 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(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", "<!DOCTYPE ncx PUBLIC \"-//NISO//DTD ncx 2005-1//EN\" \"http://www.daisy.org/z3986/2005/ncx-2005-1.dtd\">\n", "<ncx version=\"2005-1\" xml:lang=\"en-US\" xmlns=\"http://www.daisy.org/z3986/2005/ncx/\">\n", @@ -83,9 +83,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(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", "<package version=\"2.0\" unique-identifier=\"PrimaryID\" xmlns=\"http://www.idpf.org/2007/opf\">\n", "<metadata xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:opf=\"http://www.idpf.org/2007/opf\">\n", @@ -101,10 +101,10 @@ class OPFBuilder: for filename in filelist: f.write("<item id=\"page_" + filename[0] + "\" href=\"" + filename[0] + ".html\" media-type=\"application/xhtml+xml\"/>\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("<item id=\"img_" + filename[0] + "\" href=\"" + filename[0] + filename[1] + "\" media-type=\"" + mt + "\"/>\n") f.write("</manifest>\n<spine toc=\"ncx\">\n") for filename in filelist: @@ -115,7 +115,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 +134,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 <profile> <dir> <title>" % sys.argv[0] - #print " <title> is optional" parser.print_help() def main(argv=None): @@ -144,18 +141,20 @@ 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 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]") 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() @@ -168,28 +167,31 @@ def main(argv=None): try: print "Splitting double pages..." for file in os.listdir(dir): - if (getImageFileName(file) != None): + if getImageFileName(file) is not None: img = image.ComicPage(dir+'/'+file, options.profile) img.splitPage(dir, options.righttoleft) for file in os.listdir(dir): - if (getImageFileName(file) != None): + if getImageFileName(file) is not None: print "Optimizing " + file + " for " + options.profile img = image.ComicPage(dir+'/'+file, options.profile) + img.cutPageNumber() + img.cropWhiteSpace(5.0) img.resizeImage() #img.frameImage() + #img.addProgressbar() img.quantizeImage() img.saveToDir(dir) except ImportError: print "Could not load PIL, not optimizing image" 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) NCXbuilder(dir,options.title) # ensure we're sorting files alphabetically diff --git a/kcc/gui.py b/kcc/gui.py index a9b24f6..6e0a7f5 100644 --- a/kcc/gui.py +++ b/kcc/gui.py @@ -18,19 +18,20 @@ from Tkinter import * import tkFileDialog +import ttk import comic2ebook from image import ProfileData class MainWindow: def clear_files(self): - self.files = [] + self.filelist = [] self.refresh_list() def open_files(self): - filetypes = [('all files', '.*'), ('Comic files', ('*.cbr','*.cbz','*.zip','*.rar'))] + filetypes = [('all files', '.*'), ('Comic files', ('*.cbr','*.cbz','*.zip','*.rar','*.pdf'))] f = tkFileDialog.askopenfilenames(title="Choose a file...",filetypes=filetypes) - if (isinstance(f,tuple) == False): + if not isinstance(f,tuple): try: import re f = re.findall('\{(.*?)\}', f) @@ -41,17 +42,17 @@ class MainWindow: "askopenfilename() returned other than a tuple and no regex module could be found" ) sys.exit(1) - self.files.extend(f) + self.filelist.extend(f) self.refresh_list() def open_folder(self): - self.files = tkFileDialog.askdirectory(title="Choose a folder...") + self.filelist = tkFileDialog.askdirectory(title="Choose a folder...") self.refresh_list() def refresh_list(self): self.filelocation.config(state=NORMAL) self.filelocation.delete(0, END) - for file in self.files: + for file in self.filelist: self.filelocation.insert(END, file) self.filelocation.config(state=DISABLED) @@ -69,10 +70,8 @@ class MainWindow: self.profile = StringVar() self.profile.set("KHD") - for text in ProfileData.Profiles: - b = Radiobutton(self.master, text=text, - variable=self.profile, value=text) - b.pack(anchor=W,fill=BOTH) + w = apply(OptionMenu, (self.master, self.profile) + tuple(sorted(ProfileData.Profiles.iterkeys()))) + w.pack(anchor=W,fill=BOTH) self.mangastyle = BooleanVar() self.mangastyle = False @@ -84,18 +83,22 @@ class MainWindow: self.submit = Button(self.master, text="Execute!", command=self.convert, fg="red") self.submit.pack() + self.progressbar = ttk.Progressbar(orient=HORIZONTAL, length=200, mode='determinate') + self.progressbar.pack(side=BOTTOM) + def convert(self): argv = ["-p",self.profile.get()] - if (self.mangastyle == True): + if self.mangastyle: argv.append("-m") - for entry in self.files: + self.progressbar.start() + for entry in self.filelist: subargv = list(argv) subargv.append(entry) comic2ebook.main(subargv) print "Done!" def __init__(self, master, title): - self.files = [] + self.filelist = [] self.master = master self.master.title(title) self.initialize() diff --git a/kcc/image.py b/kcc/image.py index d072a78..c6042ce 100755 --- a/kcc/image.py +++ b/kcc/image.py @@ -1,4 +1,6 @@ # Copyright (C) 2010 Alex Yatskov +# Copyright (C) 2011 Stanislav (proDOOMman) Kosolapov <prodoomman@gmail.com> +# Copyright (C) 2012-2013 Ciro Mattia Gonano <ciromattia@gmail.com> # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -14,7 +16,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -from PIL import Image, ImageDraw +from PIL import Image, 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: @@ -102,15 +106,21 @@ class ComicPage: 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) + # TODO: + # - add option to stretch page + # - add option to upscale page + # - if ratio is not equal to dev size and stretch is not enabled, add white + # background and center it (otherwise K3 does not display page + # center-aligned but left-aligned) def resizeImage(self): widthDev, heightDev = self.size widthImg, heightImg = self.image.size @@ -129,19 +139,13 @@ class ComicPage: 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): + 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 +157,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 +168,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 +194,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<delta: + diff=delta + oldStat=ImageStat.Stat(self.image.crop((0,heightImg-diff,widthImg,heightImg))).var[0] + diff += delta + while ImageStat.Stat(self.image.crop((0,heightImg-diff,widthImg,heightImg))).var[0] - oldStat > 0\ + and diff < heightImg/4: + oldStat=ImageStat.Stat(self.image.crop((0,heightImg-diff,widthImg,heightImg))).var[0] + diff += delta + diff -= delta + pageNumberCut2 = diff + diff += delta + oldStat=ImageStat.Stat(self.image.crop((0,heightImg-diff,widthImg,heightImg-pageNumberCut2))).var[0] + while ImageStat.Stat(self.image.crop((0,heightImg-diff,widthImg,heightImg-pageNumberCut2))).var[0] < fixedThreshold+oldStat\ + and diff < heightImg/4: + diff += delta + diff -= delta + pageNumberCut3 = diff + delta = 5 + diff = delta + while ImageStat.Stat(self.image.crop((0,heightImg-pageNumberCut2,diff,heightImg))).var[0] < fixedThreshold and diff < widthImg: + diff += delta + diff -= delta + pageNumberX1 = diff + diff = delta + while ImageStat.Stat(self.image.crop((widthImg-diff,heightImg-pageNumberCut2,widthImg,heightImg))).var[0] < fixedThreshold and diff < widthImg: + diff += delta + diff -= delta + pageNumberX2=widthImg-diff + + if pageNumberCut3-pageNumberCut1 > 2*delta\ + and float(pageNumberX2-pageNumberX1)/float(pageNumberCut2-pageNumberCut1) <= 9.0\ + and ImageStat.Stat(self.image.crop((0,heightImg-pageNumberCut3,widthImg,heightImg))).var[0] / ImageStat.Stat(self.image).var[0] < 0.1\ + and pageNumberCut3 < heightImg/4-delta: + diff=pageNumberCut3 + else: + diff=pageNumberCut1 + self.image = self.image.crop((0,0,widthImg,heightImg-diff)) + return self.image + + def cropWhiteSpace(self, threshold): + widthImg, heightImg = self.image.size + delta = 10 + diff = delta + # top + while ImageStat.Stat(self.image.crop((0,0,widthImg,diff))).var[0] < threshold and diff < heightImg: + diff += delta + diff -= delta + # print "Top crop: %s"%diff + self.image = self.image.crop((0,diff,widthImg,heightImg)) + widthImg, heightImg = self.image.size + diff = delta + # left + while ImageStat.Stat(self.image.crop((0,0,diff,heightImg))).var[0] < threshold and diff < widthImg: + diff += delta + diff -= delta + # print "Left crop: %s"%diff + self.image = self.image.crop((diff,0,widthImg,heightImg)) + widthImg, heightImg = self.image.size + diff = delta + # down + while ImageStat.Stat(self.image.crop((0,heightImg-diff,widthImg,heightImg))).var[0] < threshold\ + and diff < heightImg: + diff += delta + diff -= delta + # print "Down crop: %s"%diff + self.image = self.image.crop((0,0,widthImg,heightImg-diff)) + widthImg, heightImg = self.image.size + diff = delta + # right + while ImageStat.Stat(self.image.crop((widthImg-diff,0,widthImg,heightImg))).var[0] < threshold\ + and diff < widthImg: + diff += delta + diff -= delta + # print "Right crop: %s"%diff + self.image = self.image.crop((0,0 ,widthImg-diff,heightImg)) + # print "New size: %sx%s"%(self.image.size[0],self.image.size[1]) + return self.image + + def addProgressbar(self, file_number, files_totalnumber, size, howoften): + if file_number//howoften!=float(file_number)/howoften: + return self.image + white = (255,255,255) + black = (0,0,0) + widthDev, heightDev = size + widthImg, heightImg = self.image.size + pastePt = ( + max(0, (widthDev - widthImg) / 2), + max(0, (heightDev - heightImg) / 2) + ) + imageBg = Image.new('RGB',size,white) + imageBg.paste(self.image, pastePt) + self.image = imageBg + widthImg, heightImg = self.image.size + draw = ImageDraw.Draw(self.image) + #Black rectangle + draw.rectangle([(0,heightImg-3), (widthImg,heightImg)], outline=black, fill=black) + #White rectangle + draw.rectangle([(widthImg*file_number/files_totalnumber,heightImg-3), (widthImg-1,heightImg)], outline=black, fill=white) + #Making notches + for i in range(1,10): + if i <= (10*file_number/files_totalnumber): + notch_colour=white #White + else: + notch_colour=black #Black + draw.line([(widthImg*float(i)/10,heightImg-3), (widthImg*float(i)/10,heightImg)],fill=notch_colour) + #The 50% + if i==5: + draw.rectangle([(widthImg/2-1,heightImg-5), (widthImg/2+1,heightImg)],outline=black,fill=notch_colour) + return self.image + |