From 5fe3aabc4f1e744b85c5aa8c596b5c00f6c63226 Mon Sep 17 00:00:00 2001 From: Ciro Mattia Gonano Date: Thu, 6 Dec 2012 11:21:56 +0100 Subject: [PATCH] Version 1.2 - comic optimization, mangling, and more coherent codebase --- KindleComicConverter.app/Contents/Info.plist | 6 +- .../Contents/Resources/Scripts/main.scpt | Bin 37534 -> 38514 bytes .../Contents/Resources/cbxarchive.py | 81 +++++++ .../Contents/Resources/comic2ebook.py | 176 ++++++--------- .../Resources/description.rtfd/TXT.rtf | 16 +- .../Contents/Resources/image.py | 207 ++++++++++++++++++ README.md | 35 ++- cbxarchive.py | 81 +++++++ comic2ebook.py | 176 ++++++--------- image.py | 207 ++++++++++++++++++ 10 files changed, 751 insertions(+), 234 deletions(-) create mode 100644 KindleComicConverter.app/Contents/Resources/cbxarchive.py create mode 100755 KindleComicConverter.app/Contents/Resources/image.py create mode 100644 cbxarchive.py create mode 100755 image.py diff --git a/KindleComicConverter.app/Contents/Info.plist b/KindleComicConverter.app/Contents/Info.plist index 639da80..600af91 100644 --- a/KindleComicConverter.app/Contents/Info.plist +++ b/KindleComicConverter.app/Contents/Info.plist @@ -55,11 +55,11 @@ name ScriptWindowState positionOfDivider - 0.0 + 568 savedFrame - 444 56 1021 972 0 0 1680 1028 + 144 338 889 690 0 0 1680 1028 selectedTabView - event log + result diff --git a/KindleComicConverter.app/Contents/Resources/Scripts/main.scpt b/KindleComicConverter.app/Contents/Resources/Scripts/main.scpt index 2db05407410e479b2738144e5302466c786b70e4..02a10ac6c58a89a1b9ba79c490217c5566f4c782 100644 GIT binary patch delta 15490 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 delta 14329 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 OV21V|_1Eds>;Dh$E&=)g diff --git a/KindleComicConverter.app/Contents/Resources/cbxarchive.py b/KindleComicConverter.app/Contents/Resources/cbxarchive.py new file mode 100644 index 0000000..18fe690 --- /dev/null +++ b/KindleComicConverter.app/Contents/Resources/cbxarchive.py @@ -0,0 +1,81 @@ +# Copyright (c) 2012 Ciro Mattia Gonano +# +# 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]) diff --git a/KindleComicConverter.app/Contents/Resources/comic2ebook.py b/KindleComicConverter.app/Contents/Resources/comic2ebook.py index 4fe1417..a4bd469 100755 --- a/KindleComicConverter.app/Contents/Resources/comic2ebook.py +++ b/KindleComicConverter.app/Contents/Resources/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(["\n", - "\n", - "\n", - "",basefilename,"\n", - "\n", - "\n", - "\n", - "
\n", - "\n", - "" - ]) - f.close() + self.file = file + filename = getImageFileName(file) + if (filename != None): + htmlfile = dstdir + '/' + filename[0] + '.html' + f = open(htmlfile, "w"); + f.writelines(["\n", + "\n", + "\n", + "",filename[0],"\n", + "\n", + "\n", + "\n", + "
\n", + "\n", + "" + ]) + 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(["\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 " % 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) diff --git a/KindleComicConverter.app/Contents/Resources/description.rtfd/TXT.rtf b/KindleComicConverter.app/Contents/Resources/description.rtfd/TXT.rtf index 0132f76..c8de892 100644 --- a/KindleComicConverter.app/Contents/Resources/description.rtfd/TXT.rtf +++ b/KindleComicConverter.app/Contents/Resources/description.rtfd/TXT.rtf @@ -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)\ -} \ No newline at end of file +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)} \ No newline at end of file diff --git a/KindleComicConverter.app/Contents/Resources/image.py b/KindleComicConverter.app/Contents/Resources/image.py new file mode 100755 index 0000000..31128fa --- /dev/null +++ b/KindleComicConverter.app/Contents/Resources/image.py @@ -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) diff --git a/README.md b/README.md index 3577e3f..1ef183c 100644 --- a/README.md +++ b/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 diff --git a/cbxarchive.py b/cbxarchive.py new file mode 100644 index 0000000..18fe690 --- /dev/null +++ b/cbxarchive.py @@ -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]) diff --git a/comic2ebook.py b/comic2ebook.py index 4fe1417..a4bd469 100755 --- a/comic2ebook.py +++ b/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,"\n", - "\n", - "\n", - "\n", - "
\n", - "\n", - "" - ]) - f.close() + self.file = file + filename = getImageFileName(file) + if (filename != None): + htmlfile = dstdir + '/' + filename[0] + '.html' + f = open(htmlfile, "w"); + f.writelines(["\n", + "\n", + "\n", + "",filename[0],"\n", + "\n", + "\n", + "\n", + "
\n", + "\n", + "" + ]) + 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(["\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 " % 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) diff --git a/image.py b/image.py new file mode 100755 index 0000000..31128fa --- /dev/null +++ b/image.py @@ -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)