about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--README.md14
-rwxr-xr-xkcc/comic2ebook.py46
-rw-r--r--kcc/gui.py29
-rwxr-xr-xkcc/image.py184
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 &copy; 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
+