mirror of
https://github.com/ciromattia/kcc
synced 2025-12-20 13:11:47 +00:00
Added a bunch of optimizations (autocontrast, page centering)
This commit is contained in:
47
README.md
47
README.md
@@ -1,32 +1,34 @@
|
|||||||
# KindleComicConverter
|
# KindleComicConverter
|
||||||
|
|
||||||
`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.
|
`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.
|
||||||
|
It also optimizes comic images by:
|
||||||
|
- enhancing contrast
|
||||||
|
- cutting page numbering
|
||||||
|
- cropping white borders
|
||||||
|
- resizing larger images to device's native resolution
|
||||||
|
- quantizing images to device's palette
|
||||||
|
|
||||||
## INPUT FORMATS
|
## INPUT FORMATS
|
||||||
`kcc` can understand and convert, at the moment, the following file types:
|
`kcc` can understand and convert, at the moment, the following file types:
|
||||||
- CBZ, ZIP
|
- CBZ, ZIP
|
||||||
- CBR, RAR
|
- CBR, RAR *(with `unrar` executable)*
|
||||||
- flat folders
|
- flat folders
|
||||||
- PDF *(extracting only contained JPG images)*
|
- PDF *(extracting only contained JPG images)*
|
||||||
|
|
||||||
For now the script does not understand folder depth, so it will work on flat folders/archives only.
|
For now the script does not understand folder depth, so it will work on flat folders/archives only.
|
||||||
|
|
||||||
## REQUIREMENTS
|
## REQUIREMENTS
|
||||||
- `kindlegen` in /usr/local/bin/
|
- `kindlegen` in /usr/local/bin/
|
||||||
- [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.
|
- [unrar](http://www.rarlab.com/download.htm) CBR support.
|
||||||
|
|
||||||
### for compiling/running from source:
|
### for compiling/running from source:
|
||||||
- Python 2.7+ (included in MacOS and Linux, follow the [official documentation](http://www.python.org/getit/windows/) to install on Windows)
|
- Python 2.7+ (included in MacOS and Linux, follow the [official documentation](http://www.python.org/getit/windows/) to install on Windows)
|
||||||
- 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.
|
- [Python Imaging Library](http://www.pythonware.com/products/pil/) for 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.
|
Please refer to official documentation for installing into your system.
|
||||||
|
|
||||||
## USAGE
|
## USAGE
|
||||||
Drop a folder or a CBZ/CBR file over the app, 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 droplet *ALWAYS* uses the **KHD** profile (*Kindle Paperwhite*).
|
### Standalone `comic2ebook.py` usage:
|
||||||
> If you want to specify other profiles, please use the script from command line.
|
|
||||||
|
|
||||||
### standalone `comic2ebook.py` usage:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
comic2ebook.py [options] comic_file|comic_folder
|
comic2ebook.py [options] comic_file|comic_folder
|
||||||
@@ -34,18 +36,32 @@ comic2ebook.py [options] comic_file|comic_folder
|
|||||||
--version show program's version number and exit
|
--version show program's version number and exit
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
-p PROFILE, --profile=PROFILE
|
-p PROFILE, --profile=PROFILE
|
||||||
Device profile (choose one among K1, K2, K3, K4, KHD
|
Device profile (choose one among K1, K2, K3, K4, KDX, KDXG or KHD) [default=KHD]
|
||||||
[default])
|
|
||||||
-t TITLE, --title=TITLE
|
-t TITLE, --title=TITLE
|
||||||
Comic title
|
Comic title [default=filename]
|
||||||
-m, --manga-style Split pages 'manga style' (right-to-left reading)
|
-m, --manga-style Split pages 'manga style' (right-to-left reading) [default=False]
|
||||||
|
-v, --verbose Verbose output [default=False]
|
||||||
|
-i, --image-processing
|
||||||
|
Apply image preprocessing (page splitting and optimizations) [default=True]
|
||||||
|
--upscale-images Resize images smaller than device's resolution [default=False]
|
||||||
|
--stretch-images Stretch images to device's resolution [default=False]
|
||||||
|
--cut-page-numbers Try to cut page numbering on images [default=True]
|
||||||
```
|
```
|
||||||
|
|
||||||
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, then:
|
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, then:
|
||||||
1. 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.
|
1. 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.
|
||||||
2. Remove the SRCS record to reduce the `.mobi` filesize in half. You can use [Kindlestrip](http://www.mobileread.com/forums/showthread.php?t=96903).
|
2. (optionally) remove the SRCS record to reduce the `.mobi` filesize in half. You can use [Kindlestrip](http://www.mobileread.com/forums/showthread.php?t=96903).
|
||||||
3. Copy the `.mobi` file to your Kindle!
|
3. Copy the `.mobi` file to your Kindle!
|
||||||
|
|
||||||
|
### AppleScript Droplet (may be outdated)
|
||||||
|
|
||||||
|
Drop a folder or a CBZ/CBR file over the app, 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 droplet *ALWAYS* uses the **KHD** profile (*Kindle Paperwhite*).
|
||||||
|
> If you want to specify other profiles, please use the script from command line.
|
||||||
|
|
||||||
|
|
||||||
## CREDITS
|
## CREDITS
|
||||||
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))
|
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))
|
||||||
|
|
||||||
@@ -71,13 +87,14 @@ and installed in `/usr/local/bin/`
|
|||||||
WARNING: PIL is required for all image mangling!
|
WARNING: PIL is required for all image mangling!
|
||||||
- 1.30 - Fixed an issue in OPF generation for device resolution
|
- 1.30 - Fixed an issue in OPF generation for device resolution
|
||||||
Reworked options system (call with -h option to get the inline help)
|
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)
|
||||||
- 2.00 - GUI! AppleScript is gone and Tk is used to provide cross-platform GUI support.
|
- 2.00 - GUI! AppleScript is gone and Tk is used to provide cross-platform GUI support.
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
- Add gracefully exit for CBR if no rarfile.py and no unrar executable are found
|
- Add gracefully exit for CBR if no rarfile.py and no unrar executable are found
|
||||||
- Improve error reporting
|
- Improve error reporting
|
||||||
- Recurse into dirtree for multiple comics
|
- Recurse into dirtree for multiple comics
|
||||||
- Support pages extraction from PDF files
|
|
||||||
|
|
||||||
## COPYRIGHT
|
## COPYRIGHT
|
||||||
|
|
||||||
|
|||||||
@@ -141,13 +141,21 @@ def main(argv=None):
|
|||||||
usage = "Usage: %prog [options] comic_file|comic_folder"
|
usage = "Usage: %prog [options] comic_file|comic_folder"
|
||||||
parser = OptionParser(usage=usage, version=__version__)
|
parser = OptionParser(usage=usage, version=__version__)
|
||||||
parser.add_option("-p", "--profile", action="store", dest="profile", default="KHD",
|
parser.add_option("-p", "--profile", action="store", dest="profile", default="KHD",
|
||||||
help="Device profile (choose one among K1, K2, K3, K4, KDX or KHD) [default=KHD]")
|
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",
|
parser.add_option("-t", "--title", action="store", dest="title", default="defaulttitle",
|
||||||
help="Comic title [default=filename]")
|
help="Comic title [default=filename]")
|
||||||
parser.add_option("-m", "--manga-style", action="store_true", dest="righttoleft", default=False,
|
parser.add_option("-m", "--manga-style", action="store_true", dest="righttoleft", default=False,
|
||||||
help="Split pages 'manga style' (right-to-left reading) [default=False]")
|
help="Split pages 'manga style' (right-to-left reading) [default=False]")
|
||||||
parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False,
|
parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False,
|
||||||
help="Verbose output [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)
|
options, args = parser.parse_args(argv)
|
||||||
if len(args) != 1:
|
if len(args) != 1:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
@@ -163,8 +171,17 @@ def main(argv=None):
|
|||||||
if cbx.isCbxFile():
|
if cbx.isCbxFile():
|
||||||
cbx.extract()
|
cbx.extract()
|
||||||
dir = cbx.getPath()
|
dir = cbx.getPath()
|
||||||
filelist = []
|
else:
|
||||||
try:
|
try:
|
||||||
|
import shutil
|
||||||
|
shutil.copytree(dir, dir + "_orig")
|
||||||
|
#dir = dir + "_orig"
|
||||||
|
except OSError as exc:
|
||||||
|
raise
|
||||||
|
filelist = []
|
||||||
|
if options.imgproc:
|
||||||
|
try:
|
||||||
|
if options.verbose:
|
||||||
print "Splitting double pages..."
|
print "Splitting double pages..."
|
||||||
for file in os.listdir(dir):
|
for file in os.listdir(dir):
|
||||||
if getImageFileName(file) is not None:
|
if getImageFileName(file) is not None:
|
||||||
@@ -172,13 +189,14 @@ def main(argv=None):
|
|||||||
img.splitPage(dir, options.righttoleft)
|
img.splitPage(dir, options.righttoleft)
|
||||||
for file in os.listdir(dir):
|
for file in os.listdir(dir):
|
||||||
if getImageFileName(file) is not None:
|
if getImageFileName(file) is not None:
|
||||||
|
if options.verbose:
|
||||||
print "Optimizing " + file + " for " + options.profile
|
print "Optimizing " + file + " for " + options.profile
|
||||||
img = image.ComicPage(dir+'/'+file, options.profile)
|
img = image.ComicPage(dir+'/'+file, options.profile)
|
||||||
|
img.optimizeImage()
|
||||||
|
img.cropWhiteSpace(10.0)
|
||||||
|
if options.cutpagenumbers:
|
||||||
img.cutPageNumber()
|
img.cutPageNumber()
|
||||||
img.cropWhiteSpace(5.0)
|
img.resizeImage(options.upscale,options.stretch)
|
||||||
img.resizeImage()
|
|
||||||
#img.frameImage()
|
|
||||||
#img.addProgressbar()
|
|
||||||
img.quantizeImage()
|
img.quantizeImage()
|
||||||
img.saveToDir(dir)
|
img.saveToDir(dir)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -193,11 +211,14 @@ def main(argv=None):
|
|||||||
filename = HTMLbuilder(dir,file).getResult()
|
filename = HTMLbuilder(dir,file).getResult()
|
||||||
if filename is not None:
|
if filename is not None:
|
||||||
filelist.append(filename)
|
filelist.append(filename)
|
||||||
|
if options.title == 'defaulttitle':
|
||||||
|
options.title = os.path.basename(dir)
|
||||||
NCXbuilder(dir,options.title)
|
NCXbuilder(dir,options.title)
|
||||||
# ensure we're sorting files alphabetically
|
# ensure we're sorting files alphabetically
|
||||||
filelist = sorted(filelist, key=lambda name: name[0])
|
filelist = sorted(filelist, key=lambda name: name[0])
|
||||||
OPFBuilder(options.profile,dir,options.title,filelist)
|
OPFBuilder(options.profile,dir,options.title,filelist)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
Copyright()
|
Copyright()
|
||||||
main(sys.argv[1:])
|
main(sys.argv[1:])
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ class MainWindow:
|
|||||||
subargv = list(argv)
|
subargv = list(argv)
|
||||||
subargv.append(entry)
|
subargv.append(entry)
|
||||||
comic2ebook.main(subargv)
|
comic2ebook.main(subargv)
|
||||||
|
|
||||||
print "Done!"
|
print "Done!"
|
||||||
|
|
||||||
def __init__(self, master, title):
|
def __init__(self, master, title):
|
||||||
|
|||||||
53
kcc/image.py
53
kcc/image.py
@@ -16,7 +16,7 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from PIL import Image, ImageDraw, ImageStat
|
from PIL import Image, ImageOps, ImageDraw, ImageStat
|
||||||
|
|
||||||
class ImageFlags:
|
class ImageFlags:
|
||||||
Orient = 1 << 0
|
Orient = 1 << 0
|
||||||
@@ -103,6 +103,9 @@ class ComicPage:
|
|||||||
except IOError as e:
|
except IOError as e:
|
||||||
raise RuntimeError('Cannot write image in directory %s: %s' %(targetdir,e))
|
raise RuntimeError('Cannot write image in directory %s: %s' %(targetdir,e))
|
||||||
|
|
||||||
|
def optimizeImage(self):
|
||||||
|
self.image = ImageOps.autocontrast(self.image)
|
||||||
|
|
||||||
def quantizeImage(self):
|
def quantizeImage(self):
|
||||||
colors = len(self.palette) / 3
|
colors = len(self.palette) / 3
|
||||||
if colors < 256:
|
if colors < 256:
|
||||||
@@ -111,33 +114,31 @@ class ComicPage:
|
|||||||
palImg.putpalette(self.palette)
|
palImg.putpalette(self.palette)
|
||||||
self.image = self.image.quantize(palette=palImg)
|
self.image = self.image.quantize(palette=palImg)
|
||||||
|
|
||||||
def stretchImage(self):
|
def resizeImage(self,upscale=False, stretch=False):
|
||||||
widthDev, heightDev = self.size
|
method = Image.ANTIALIAS
|
||||||
self.image = self.image.resize((widthDev, heightDev), Image.ANTIALIAS)
|
if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]:
|
||||||
|
if not upscale:
|
||||||
# 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
|
|
||||||
if widthImg <= widthDev and heightImg <= heightDev:
|
|
||||||
return self.image
|
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:
|
else:
|
||||||
widthImg, heightImg = self.size
|
method = Image.NEAREST
|
||||||
self.image = self.image.resize((widthImg, heightImg), Image.ANTIALIAS)
|
|
||||||
|
if stretch: # if stretching call directly resize() without other considerations.
|
||||||
|
self.image = self.image.resize(self.size,method)
|
||||||
|
return self.image
|
||||||
|
|
||||||
|
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):
|
def splitPage(self, targetdir, righttoleft=False):
|
||||||
width, height = self.image.size
|
width, height = self.image.size
|
||||||
|
|||||||
Reference in New Issue
Block a user