diff --git a/kcc/comic2ebook.py b/kcc/comic2ebook.py index bcb87ff..ebc721d 100755 --- a/kcc/comic2ebook.py +++ b/kcc/comic2ebook.py @@ -127,7 +127,7 @@ def buildNCX(dstdir, title, chapters): def buildOPF(profile, dstdir, title, filelist, cover=None, righttoleft=False): opffile = os.path.join(dstdir, 'OEBPS', 'content.opf') # read the first file resolution - profilelabel, deviceres, palette = image.ProfileData.Profiles[profile] + profilelabel, deviceres, palette, gamma = image.ProfileData.Profiles[profile] imgres = str(deviceres[0]) + "x" + str(deviceres[1]) if righttoleft: writingmode = "horizontal-rl" @@ -252,12 +252,13 @@ def isInFilelist(filename, filelist): def applyImgOptimization(img, isSplit=False, toRight=False): - img.optimizeImage() + img.optimizeImage(options.gamma) img.cropWhiteSpace(10.0) if options.cutpagenumbers: img.cutPageNumber() img.resizeImage(options.upscale, options.stretch, options.black_borders, isSplit, toRight) - img.quantizeImage() + if not options.notquantize: + img.quantizeImage() def dirImgProcess(path): @@ -277,7 +278,10 @@ def dirImgProcess(path): else: print ".", img = image.ComicPage(os.path.join(dirpath, afile), options.profile) - split = img.splitPage(dirpath, options.righttoleft, options.rotate) + if options.nosplitrotate: + split = None + else: + split = img.splitPage(dirpath, options.righttoleft, options.rotate) if split is not None: if options.verbose: print "Splitted " + afile @@ -297,17 +301,17 @@ def dirImgProcess(path): facing = "left" img0 = image.ComicPage(split[0], options.profile) applyImgOptimization(img0, True, toRight1) - img0.saveToDir(dirpath) + img0.saveToDir(dirpath, options.notquantize) img1 = image.ComicPage(split[1], options.profile) applyImgOptimization(img1, True, toRight2) - img1.saveToDir(dirpath) + img1.saveToDir(dirpath, options.notquantize) else: if facing == "right": facing = "left" else: facing = "right" applyImgOptimization(img) - img.saveToDir(dirpath) + img.saveToDir(dirpath, options.notquantize) def genEpubStruct(path): @@ -396,6 +400,10 @@ def main(argv=None): help="Verbose output [default=False]") parser.add_option("--no-image-processing", action="store_false", dest="imgproc", default=True, help="Do not apply image preprocessing (page splitting and optimizations) [default=True]") + parser.add_option("--gamma", type="float", dest="gamma", default="0.0", + help="Apply gamma correction to linearize the image [default=auto]") + parser.add_option("--nodithering", action="store_true", dest="notquantize", default=False, + help="Disable image quantization [default=False]") 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, @@ -405,6 +413,8 @@ def main(argv=None): + "is not like the device's one [default=False]") parser.add_option("--no-cut-page-numbers", action="store_false", dest="cutpagenumbers", default=True, help="Do not try to cut page numbering on images [default=True]") + parser.add_option("--nosplitrotate", action="store_true", dest="nosplitrotate", default=False, + help="Disable splitting and rotation [default=False]") parser.add_option("--rotate", action="store_true", dest="rotate", default=False, help="Rotate landscape pages instead of splitting them [default=False]") parser.add_option("-o", "--output", action="store", dest="output", default=None, diff --git a/kcc/gui.py b/kcc/gui.py index 6ba7e4d..ad068a7 100644 --- a/kcc/gui.py +++ b/kcc/gui.py @@ -41,24 +41,19 @@ class MainWindow: self.refresh_list() def open_files(self): - filetypes = [('all files', '.*'), ('Comic files', ('*.cbr', '*.cbz', '*.zip', '*.rar', '*.pdf'))] - f = tkFileDialog.askopenfilenames(title="Choose a file...", filetypes=filetypes) + filetypes = [('All files', '.*'), ('Comic files', ('*.cbr', '*.cbz', '*.zip', '*.rar', '*.pdf'))] + f = tkFileDialog.askopenfilenames(title="Choose files", filetypes=filetypes) if not isinstance(f, tuple): try: import re f = re.findall('\{(.*?)\}', f) except: - import tkMessageBox - tkMessageBox.showerror( - "Open file", - "askopenfilename() returned other than a tuple and no regex module could be found" - ) sys.exit(1) self.filelist.extend(f) self.refresh_list() def open_folder(self): - f = tkFileDialog.askdirectory(title="Choose a folder...") + f = tkFileDialog.askdirectory(title="Choose folder:") self.filelist.extend([f]) self.refresh_list() @@ -76,63 +71,86 @@ class MainWindow: self.clear_file = Button(self.master, text="Clear files", command=self.clear_files) self.clear_file.grid(row=4, column=0, rowspan=3) - self.open_file = Button(self.master, text="Add files...", command=self.open_files) + self.open_file = Button(self.master, text="Add files", command=self.open_files) self.open_file.grid(row=4, column=1, rowspan=3) - self.open_folder = Button(self.master, text="Add folder...", command=self.open_folder) + self.open_folder = Button(self.master, text="Add folder", command=self.open_folder) self.open_folder.grid(row=4, column=2, rowspan=3) self.profile = StringVar() profiles = sorted(ProfileData.ProfileLabels.iterkeys()) self.profile.set(profiles[-1]) w = apply(OptionMenu, (self.master, self.profile) + tuple(profiles)) - w.grid(row=1, column=3) + w.grid(row=4, column=3, sticky=W + E + N + S) self.options = { 'epub_only': IntVar(None, 0), 'image_preprocess': IntVar(None, 1), + 'notquantize': IntVar(None, 0), + 'nosplitrotate': IntVar(None, 0), 'rotate': IntVar(None, 0), 'cut_page_numbers': IntVar(None, 1), 'mangastyle': IntVar(None, 0), + 'image_gamma': DoubleVar(None, 0.0), 'image_upscale': IntVar(None, 0), 'image_stretch': IntVar(None, 0), 'black_borders': IntVar(None, 0) } self.optionlabels = { - 'epub_only': "Generate ePub only (does not call 'kindlegen')", + 'epub_only': "Generate EPUB only", 'image_preprocess': "Apply image optimizations", + 'notquantize': "Disable image quantization", + 'nosplitrotate': "Disable splitting and rotation", 'rotate': "Rotate landscape images instead of splitting them", 'cut_page_numbers': "Cut page numbers", - 'mangastyle': "Manga-style (right-to-left reading, applies to reading and splitting)", + 'mangastyle': "Manga mode", + 'image_gamma': "Custom gamma\n(if 0.0 the default gamma for the profile will be used)", 'image_upscale': "Allow image upscaling", 'image_stretch': "Stretch images", 'black_borders': "Use black borders" } for key in self.options: - aCheckButton = Checkbutton(self.master, text=self.optionlabels[key], variable=self.options[key]) - aCheckButton.grid(column=3, sticky='w') - self.progressbar = ttk.Progressbar(orient=HORIZONTAL, length=200, mode='determinate') + if isinstance(self.options[key], IntVar) or isinstance(self.options[key], BooleanVar): + aCheckButton = Checkbutton(self.master, text=self.optionlabels[key], variable=self.options[key]) + aCheckButton.grid(columnspan=4, sticky=W + N + S) + elif isinstance(self.options[key], DoubleVar): + aLabel = Label(self.master, text=self.optionlabels[key], justify=RIGHT) + aLabel.grid(column=0, columnspan=3, sticky=W + N + S) + aEntry = Entry(self.master, textvariable=self.options[key]) + aEntry.grid(column=3, row=(self.master.grid_size()[1] - 1), sticky=W + N + S) - self.submit = Button(self.master, text="Execute!", command=self.start_conversion, fg="red") - self.submit.grid(column=3) - self.progressbar.grid(column=0, columnspan=4, sticky=W + E + N + S) - - self.notelabel = Label(self.master, - text="GUI can seem frozen while converting, kindly wait until some message appears!") - self.notelabel.grid(column=0, columnspan=4, sticky=W + E + N + S) + self.submit = Button(self.master, text="CONVERT", command=self.start_conversion, fg="red") + self.submit.grid(columnspan=4, sticky=W + E + N + S) + aLabel = Label(self.master, text="file progress", justify=RIGHT) + aLabel.grid(column=0, sticky=E) + self.progress_file = ttk.Progressbar(orient=HORIZONTAL, length=200, mode='determinate', maximum=4) + self.progress_file.grid(column=1, columnspan=3, row=(self.master.grid_size()[1] - 1), sticky=W + E + N + S) + aLabel = Label(self.master, text="overall progress", justify=RIGHT) + aLabel.grid(column=0, sticky=E) + self.progress_overall = ttk.Progressbar(orient=HORIZONTAL, length=200, mode='determinate') + self.progress_overall.grid(column=1, columnspan=3, row=(self.master.grid_size()[1] - 1), sticky=W + E + N + S) def start_conversion(self): - self.progressbar.start() + self.submit['state'] = DISABLED + self.master.update() self.convert() - self.progressbar.stop() + self.submit['state'] = NORMAL + self.master.update() def convert(self): if len(self.filelist) < 1: - tkMessageBox.showwarning('No file selected', "You should really select some files to convert...") + tkMessageBox.showwarning('No files selected!', "Please choose files to convert.") return profilekey = ProfileData.ProfileLabels[self.profile.get()] argv = ["-p", profilekey] + if self.options['image_gamma'].get() != 0.0: + argv.append("--gamma") + argv.append(self.options['image_gamma'].get()) if self.options['image_preprocess'].get() == 0: argv.append("--no-image-processing") + if self.options['notquantize'].get() == 1: + argv.append("--nodithering") + if self.options['nosplitrotate'].get() == 1: + argv.append("--nosplitrotate") if self.options['rotate'].get() == 1: argv.append("--rotate") if self.options['cut_page_numbers'].get() == 0: @@ -146,12 +164,20 @@ class MainWindow: if self.options['black_borders'].get() == 1: argv.append("--black-borders") errors = False + left_files = len(self.filelist) + filenum = 0 + self.progress_overall['value'] = 0 + self.progress_overall['maximum'] = left_files for entry in self.filelist: + filenum += 1 + self.progress_file['value'] = 1 self.master.update() subargv = list(argv) try: subargv.append(entry) epub_path = comic2ebook.main(subargv) + self.progress_file['value'] = 2 + self.master.update() except Exception as err: type_, value_, traceback_ = sys.exc_info() tkMessageBox.showerror('KCC Error', "Error on file %s:\n%s\nTraceback:\n%s" % @@ -166,8 +192,10 @@ class MainWindow: print >>sys.stderr, "Child was terminated by signal", -retcode else: print >>sys.stderr, "Child returned", retcode + self.progress_file['value'] = 3 + self.master.update() except OSError as e: - tkMessageBox.showerror('Error kindlegen', "Error on file %s:\n%s" % (epub_path, e)) + tkMessageBox.showerror('KindleGen Error', "Error on file %s:\n%s" % (epub_path, e)) errors = True continue mobifile = epub_path.replace('.epub', '.mobi') @@ -175,19 +203,23 @@ class MainWindow: shutil.move(mobifile, mobifile + '_tostrip') kindlestrip.main((mobifile + '_tostrip', mobifile)) os.remove(mobifile + '_tostrip') + self.progress_file['value'] = 4 + self.master.update() except Exception, err: tkMessageBox.showerror('Error', "Error on file %s:\n%s" % (mobifile, str(err))) errors = True continue + self.progress_overall['value'] = filenum + self.master.update() if errors: tkMessageBox.showinfo( "Done", - "Conversion finished (some errors have been reported)" + "Conversion failed. Errors have been reported." ) else: tkMessageBox.showinfo( "Done", - "Conversion successfully done!" + "Conversion successful!" ) def remove_readonly(self, fn, path): diff --git a/kcc/image.py b/kcc/image.py index f3080ed..8eeae69 100755 --- a/kcc/image.py +++ b/kcc/image.py @@ -77,13 +77,13 @@ class ProfileData: ] Profiles = { - '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) + 'K1': ("Kindle", (600, 800), Palette4, 1.8), + 'K2': ("Kindle 2", (600, 800), Palette15, 1.8), + 'K3': ("Kindle 3/Keyboard", (600, 800), Palette16, 1.8), + 'K4': ("Kindle 4/NT/Touch", (600, 800), Palette16, 1.8), + 'KHD': ("Kindle Paperwhite", (758, 1024), Palette16, 1.8), + 'KDX': ("Kindle DX", (824, 1200), Palette15, 1.8), + 'KDXG': ("Kindle DXG", (824, 1200), Palette16, 1.8) } ProfileLabels = { @@ -101,7 +101,7 @@ class ComicPage: def __init__(self, source, device): try: self.profile = device - self.profile_label, self.size, self.palette = ProfileData.Profiles[device] + self.profile_label, self.size, self.palette, self.gamma = ProfileData.Profiles[device] except KeyError: raise RuntimeError('Unexpected output device %s' % device) try: @@ -111,16 +111,25 @@ class ComicPage: raise RuntimeError('Cannot read image file %s' % source) self.image = self.image.convert('RGB') - def saveToDir(self, targetdir): + def saveToDir(self, targetdir, notquantize): filename = os.path.basename(self.origFileName) try: self.image = self.image.convert('L') # convert to grayscale - self.image.save(os.path.join(targetdir, filename), "JPEG") + os.remove(os.path.join(targetdir, filename)) + if notquantize: + self.image.save(os.path.join(targetdir, os.path.splitext(filename)[0] + ".jpg"), "JPEG") + else: + self.image.save(os.path.join(targetdir, os.path.splitext(filename)[0] + ".png"), "PNG") except IOError as e: raise RuntimeError('Cannot write image in directory %s: %s' % (targetdir, e)) - def optimizeImage(self): - self.image = ImageOps.autocontrast(self.image) + def optimizeImage(self, gamma): + if gamma < 0.1: + gamma = self.gamma + if gamma == 1.0: + self.image = ImageOps.autocontrast(self.image) + else: + self.image = ImageOps.autocontrast(Image.eval(self.image, lambda a: 255 * (a / 255.) ** gamma)) def quantizeImage(self): colors = len(self.palette) / 3 diff --git a/setup.py b/setup.py index 3e9e43d..dcaedf6 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ Will automatically ensure that all build prerequisites are available via ez_setup Usage (Mac OS X): - python setup.py build + python setup.py py2app Usage (Windows): python setup.py build @@ -71,7 +71,7 @@ setup( 'Intended Audience :: End Users/Desktop', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 2.7', 'Topic :: Multimedia :: Graphics :: Graphics Conversion', 'Topic :: Utilities' ],