diff --git a/installer/linux/freeze.py b/installer/linux/freeze.py
index 4627ad90ad..34c2b0d10d 100644
--- a/installer/linux/freeze.py
+++ b/installer/linux/freeze.py
@@ -19,6 +19,8 @@ EXTRAS = ('/usr/lib/python2.5/site-packages/PIL', os.path.expanduser('~/
SQLITE = '/usr/lib/libsqlite3.so.0'
DBUS = '/usr/lib/libdbus-1.so.3'
LIBMNG = '/usr/lib/libmng.so.1'
+LIBZ = '/lib/libz.so.1'
+LIBUSB = '/lib/libusb.so'
CALIBRESRC = os.path.join(CALIBREPREFIX, 'src')
@@ -117,7 +119,7 @@ binaries += [('clit', CLIT, 'BINARY'), ('pdftohtml', PDFTOHTML, 'BINARY'),
('libunrar.so', LIBUNRAR, 'BINARY')]
print 'Adding external libraries...'
-binaries += [ (os.path.basename(x), x, 'BINARY') for x in (SQLITE, DBUS, LIBMNG)]
+binaries += [ (os.path.basename(x), x, 'BINARY') for x in (SQLITE, DBUS, LIBMNG, LIBZ, LIBUSB)]
qt = []
diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py
index 68d202f201..8caebc5be2 100644
--- a/src/calibre/__init__.py
+++ b/src/calibre/__init__.py
@@ -26,6 +26,7 @@ terminal_controller = TerminalController(sys.stdout)
iswindows = 'win32' in sys.platform.lower() or 'win64' in sys.platform.lower()
isosx = 'darwin' in sys.platform.lower()
islinux = not(iswindows or isosx)
+isfrozen = hasattr(sys, 'frozen')
try:
locale.setlocale(locale.LC_ALL, '')
@@ -351,11 +352,11 @@ def get_proxies(self):
def browser(honor_time=False):
- http_proxy = get_proxies().get('http', None)
opener = mechanize.Browser()
opener.set_handle_refresh(True, honor_time=honor_time)
opener.set_handle_robots(False)
opener.addheaders = [('User-agent', 'Mozilla/5.0 (X11; U; i686 Linux; en_US; rv:1.8.0.4) Gecko/20060508 Firefox/1.5.0.4')]
+ http_proxy = get_proxies().get('http', None)
if http_proxy:
opener.set_proxies({'http':http_proxy})
return opener
diff --git a/src/calibre/devices/libusb.py b/src/calibre/devices/libusb.py
index 7d5be468f2..1502772bf4 100644
--- a/src/calibre/devices/libusb.py
+++ b/src/calibre/devices/libusb.py
@@ -8,7 +8,7 @@ from ctypes import cdll, POINTER, byref, pointer, Structure, \
c_ubyte, c_ushort, c_int, c_char, c_void_p, c_byte, c_uint
from errno import EBUSY, ENOMEM
-from calibre import iswindows, isosx, load_library
+from calibre import iswindows, isosx, load_library, isfrozen
_libusb_name = 'libusb'
PATH_MAX = 511 if iswindows else 1024 if isosx else 4096
diff --git a/src/calibre/ebooks/__init__.py b/src/calibre/ebooks/__init__.py
index dd7e8fc96a..2716963e8d 100644
--- a/src/calibre/ebooks/__init__.py
+++ b/src/calibre/ebooks/__init__.py
@@ -17,4 +17,4 @@ class UnknownFormatError(Exception):
BOOK_EXTENSIONS = ['lrf', 'rar', 'zip', 'rtf', 'lit', 'txt', 'htm', 'xhtm',
'html', 'xhtml', 'epub', 'pdf', 'prc', 'mobi', 'azw',
- 'epub', 'fb2', 'djvu', 'lrx']
+ 'epub', 'fb2', 'djvu', 'lrx', 'cbr', 'cbz']
diff --git a/src/calibre/ebooks/lrf/comic/convert_from.py b/src/calibre/ebooks/lrf/comic/convert_from.py
index 829e331526..847b588aef 100755
--- a/src/calibre/ebooks/lrf/comic/convert_from.py
+++ b/src/calibre/ebooks/lrf/comic/convert_from.py
@@ -10,8 +10,9 @@ Based on ideas from comiclrf created by FangornUK.
import os, sys, traceback, shutil
from uuid import uuid4
-from calibre import extract, OptionParser, detect_ncpus, terminal_controller, \
+from calibre import extract, detect_ncpus, terminal_controller, \
__appname__, __version__
+from calibre.utils.config import Config, StringConfig
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.utils.threadpool import ThreadPool, WorkRequest
from calibre.utils.terminfo import ProgressBar
@@ -21,7 +22,7 @@ try:
NewMagickWand, NewPixelWand, \
MagickSetImageBorderColor, \
MagickReadImage, MagickRotateImage, \
- MagickTrimImage, \
+ MagickTrimImage, PixelSetColor,\
MagickNormalizeImage, MagickGetImageWidth, \
MagickGetImageHeight, \
MagickResizeImage, MagickSetImageType, \
@@ -133,7 +134,7 @@ class PageProcessor(list):
pw = NewPixelWand()
if pw < 0:
raise RuntimeError('Cannot create wand.')
- #flag = PixelSetColor(pw, 'white')
+ PixelSetColor(pw, 'white')
MagickSetImageBorderColor(wand, pw)
@@ -145,7 +146,7 @@ class PageProcessor(list):
MagickSetImagePage(wand, 0,0,0,0) #Clear page after trim, like a "+repage"
# Do the Photoshop "Auto Levels" equivalent
- if self.opts.normalize:
+ if not self.opts.dont_normalize:
MagickNormalizeImage(wand)
sizex = MagickGetImageWidth(wand)
@@ -173,7 +174,7 @@ class PageProcessor(list):
else:
MagickResizeImage(wand, SCRWIDTH, SCRHEIGHT, CatromFilter, 1.0)
- if self.opts.sharpen:
+ if not self.opts.dont_sharpen:
MagickSharpenImage(wand, 0.0, 1.0)
MagickSetImageType(wand, GrayscaleType)
@@ -225,34 +226,46 @@ def process_pages(pages, opts, update):
finally:
finalize()
+def config(defaults=None):
+ desc = _('Options to control the conversion of comics (CBR, CBZ) files into ebooks')
+ if defaults is None:
+ c = Config('comic', desc)
+ else:
+ c = StringConfig(defaults, desc)
+ c.add_opt('title', ['-t', '--title'],
+ help=_('Title for generated ebook. Default is to use the filename.'))
+ c.add_opt('author', ['-a', '--author'],
+ help=_('Set the author in the metadata of the generated ebook. Default is %default'),
+ default=_('Unknown'))
+ c.add_opt('output', ['-o', '--output'],
+ help=_('Path to output LRF file. By default a file is created in the current directory.'))
+ c.add_opt('colors', ['-c', '--colors'], type='int', default=64,
+ help=_('Number of colors for Grayscale image conversion. Default: %default'))
+ c.add_opt('dont_normalize', ['-n', '--disable-normalize'], default=False,
+ help=_('Disable normalize (improve contrast) color range for pictures. Default: False'))
+ c.add_opt('keep_aspect_ratio', ['-r', '--keep-aspect-ratio'], default=False,
+ help=_('Maintain picture aspect ratio. Default is to fill the screen.'))
+ c.add_opt('dont_sharpen', ['-s', '--disable-sharpen'], default=False,
+ help=_('Disable sharpening.'))
+ c.add_opt('landscape', ['-l', '--landscape'], default=False,
+ help=_("Don't split landscape images into two portrait images"))
+ c.add_opt('no_sort', ['--no-sort'], default=False,
+ help=_("Don't sort the files found in the comic alphabetically by name. Instead use the order they were added to the comic."))
+ c.add_opt('profile', ['-p', '--profile'], default='prs500', choices=PROFILES.keys(),
+ help=_('Choose a profile for the device you are generating this LRF for. The default is the SONY PRS-500 with a screen size of 584x754 pixels. Choices are %s')%PROFILES.keys())
+ c.add_opt('verbose', ['--verbose'], default=0, action='count',
+ help=_('Be verbose, useful for debugging. Can be specified multiple times for greater verbosity.'))
+ c.add_opt('no_progress_bar', ['--no-progress-bar'], default=False,
+ help=_("Don't show progress bar."))
+ return c
+
def option_parser():
- parser = OptionParser(_('''\
+ c = config()
+ return c.option_parser(usage=_('''\
%prog [options] comic.cb[z|r]
Convert a comic in a CBZ or CBR file to an LRF ebook.
- '''))
- parser.add_option('-t', '--title', help=_('Title for generated ebook. Default is to use the filename.'), default=None)
- parser.add_option('-a', '--author', help=_('Set the author in the metadata of the generated ebook. Default is %default'), default=_('Unknown'))
- parser.add_option('-o', '--output', help=_('Path to output LRF file. By default a file is created in the current directory.'), default=None)
- parser.add_option('-c', '--colors', type='int', default=64,
- help=_('Number of colors for Grayscale image conversion. Default: %default'))
- parser.add_option('-n', '--disable-normalize', dest='normalize', default=True, action='store_false',
- help=_('Disable normalize (improve contrast) color range for pictures. Default: False'))
- parser.add_option('-r', '--keep-aspect-ratio', action='store_true', default=False,
- help=_('Maintain picture aspect ratio. Default is to fill the screen.'))
- parser.add_option('-s', '--disable-sharpen', default=True, action='store_false', dest='sharpen',
- help=_('Disable sharpening.'))
- parser.add_option('-l', '--landscape', default=False, action='store_true',
- help=_("Don't split landscape images into two portrait images"))
- parser.add_option('--no-sort', default=False, action='store_true',
- help=_("Don't sort the files found in the comic alphabetically by name. Instead use the order they were added to the comic."))
- parser.add_option('-p', '--profile', default='prs500', dest='profile', type='choice',
- choices=PROFILES.keys(), help=_('Choose a profile for the device you are generating this LRF for. The default is the SONY PRS-500 with a screen size of 584x754 pixels. Choices are %s')%PROFILES.keys())
- parser.add_option('--verbose', default=False, action='store_true',
- help=_('Be verbose, useful for debugging'))
- parser.add_option('--no-progress-bar', default=False, action='store_true',
- help=_("Don't show progress bar."))
- return parser
+'''))
def create_lrf(pages, profile, opts, thumbnail=None):
width, height = PROFILES[profile]
@@ -277,22 +290,8 @@ def create_lrf(pages, profile, opts, thumbnail=None):
book.renderLrf(open(opts.output, 'wb'))
-
-
-def main(args=sys.argv, notification=None):
- parser = option_parser()
- opts, args = parser.parse_args(args)
- if len(args) < 2:
- parser.print_help()
- print '\nYou must specify a file to convert'
- return 1
-
- if not callable(notification):
- pb = ProgressBar(terminal_controller, _('Rendering comic pages...'),
- no_progress_bar=opts.no_progress_bar)
- notification = pb.update
-
- source = os.path.abspath(args[1])
+def do_convert(path_to_file, opts, notification=lambda m, p: p):
+ source = path_to_file
if not opts.title:
opts.title = os.path.splitext(os.path.basename(source))
if not opts.output:
@@ -315,6 +314,24 @@ def main(args=sys.argv, notification=None):
create_lrf(pages, opts.profile, opts, thumbnail=thumbnail)
shutil.rmtree(tdir)
shutil.rmtree(tdir2)
+
+
+def main(args=sys.argv, notification=None):
+ parser = option_parser()
+ opts, args = parser.parse_args(args)
+ if len(args) < 2:
+ parser.print_help()
+ print '\nYou must specify a file to convert'
+ return 1
+
+ if not callable(notification):
+ pb = ProgressBar(terminal_controller, _('Rendering comic pages...'),
+ no_progress_bar=opts.no_progress_bar)
+ notification = pb.update
+
+ source = os.path.abspath(args[1])
+ do_convert(source, opts, notification)
+ print _('Output written to'), opts.output
return 0
if __name__ == '__main__':
diff --git a/src/calibre/gui2/dialogs/comicconf.py b/src/calibre/gui2/dialogs/comicconf.py
new file mode 100644
index 0000000000..7f42750fdd
--- /dev/null
+++ b/src/calibre/gui2/dialogs/comicconf.py
@@ -0,0 +1,85 @@
+from __future__ import with_statement
+__license__ = 'GPL v3'
+__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
+__docformat__ = 'restructuredtext en'
+
+''''''
+from PyQt4.QtGui import QDialog
+from calibre.gui2.dialogs.comicconf_ui import Ui_Dialog
+from calibre.gui2 import qstring_to_unicode
+from calibre.ebooks.lrf.comic.convert_from import config
+
+def set_conversion_defaults(window):
+ d = ComicConf(window)
+ d.exec_()
+
+def get_bulk_conversion_options(window):
+ c = config(None)
+ with open(c.config_file_path, 'rb') as f:
+ d = ComicConf(window, config_defaults=f.read())
+ if d.exec_() == QDialog.Accepted:
+ return d.config.parse()
+
+def get_conversion_options(window, defaults, title, author):
+ if defaults is None:
+ c = config(None)
+ with open(c.config_file_path, 'rb') as f:
+ defaults = f.read()
+ defaults += '\ntitle=%s\nauthor=%s'%(repr(title), repr(author))
+ d = ComicConf(window, config_defaults=defaults, generic=False)
+ if d.exec_() == QDialog.Accepted:
+ return d.config.parse(), d.config.src
+ return None, None
+
+
+class ComicConf(QDialog, Ui_Dialog):
+
+ def __init__(self, window, config_defaults=None, generic=True,
+ title=_('Set defaults for conversion of comics (CBR/CBZ files)')):
+ QDialog.__init__(self, window)
+ Ui_Dialog.__init__(self)
+ self.setupUi(self)
+ self.setWindowTitle(title)
+ self.config = config(config_defaults)
+ opts = self.config.parse()
+ if generic:
+ for i in ('title', 'author'):
+ getattr(self, 'opt_'+i).setVisible(False)
+ getattr(self, i+'_label').setVisible(False)
+ else:
+ title = opts.title
+ if not title:
+ title = _('Unknown')
+ self.setWindowTitle(_('Set options for converting %s')%title)
+ author = opts.author
+ self.opt_title.setText(title)
+ self.opt_author.setText(author)
+ self.opt_colors.setValue(opts.colors)
+ self.opt_profile.addItem(opts.profile)
+ self.opt_dont_normalize.setChecked(opts.dont_normalize)
+ self.opt_keep_aspect_ratio.setChecked(opts.keep_aspect_ratio)
+ self.opt_dont_sharpen.setChecked(opts.dont_sharpen)
+ self.opt_landscape.setChecked(opts.landscape)
+ self.opt_no_sort.setChecked(opts.no_sort)
+
+ for opt in self.config.option_set.preferences:
+ g = getattr(self, 'opt_'+opt.name, False)
+ if opt.help and g:
+ g.setToolTip(opt.help)
+
+ def accept(self):
+ for opt in self.config.option_set.preferences:
+ g = getattr(self, 'opt_'+opt.name, False)
+ if not g or not g.isVisible(): continue
+ if hasattr(g, 'isChecked'):
+ val = bool(g.isChecked())
+ elif hasattr(g, 'value'):
+ val = g.value()
+ elif hasattr(g, 'itemText'):
+ val = qstring_to_unicode(g.itemText(g.currentIndex()))
+ elif hasattr(g, 'text'):
+ val = qstring_to_unicode(g.text())
+ else:
+ raise Exception('Bad coding')
+ self.config.set(opt.name, val)
+ return QDialog.accept(self)
\ No newline at end of file
diff --git a/src/calibre/gui2/dialogs/comicconf.ui b/src/calibre/gui2/dialogs/comicconf.ui
new file mode 100644
index 0000000000..9e6198eaf7
--- /dev/null
+++ b/src/calibre/gui2/dialogs/comicconf.ui
@@ -0,0 +1,166 @@
+
+ Dialog
+
+
+
+ 0
+ 0
+ 646
+ 468
+
+
+
+ Dialog
+
+
+
+ :/images/convert.svg:/images/convert.svg
+
+
+ -
+
+
+ &Title:
+
+
+ opt_title
+
+
+
+ -
+
+
+ -
+
+
+ &Author(s):
+
+
+ opt_author
+
+
+
+ -
+
+
+ -
+
+
+ &Number of Colors:
+
+
+ opt_colors
+
+
+
+ -
+
+
+ 8
+
+
+ 3200000
+
+
+ 8
+
+
+
+ -
+
+
+ &Profile:
+
+
+ opt_profile
+
+
+
+ -
+
+
+ -
+
+
+ Disable &normalize
+
+
+
+ -
+
+
+ Keep &aspect ratio
+
+
+
+ -
+
+
+ Disable &Sharpening
+
+
+
+ -
+
+
+ &Landscape
+
+
+
+ -
+
+
+ Dont so&rt
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ Dialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ Dialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py
index c1ca797d41..32e400dafd 100644
--- a/src/calibre/gui2/main.py
+++ b/src/calibre/gui2/main.py
@@ -37,6 +37,7 @@ from calibre.gui2.dialogs.lrf_single import LRFSingleDialog, LRFBulkDialog
from calibre.gui2.dialogs.config import ConfigDialog
from calibre.gui2.dialogs.search import SearchDialog
from calibre.gui2.dialogs.user_profiles import UserProfiles
+import calibre.gui2.dialogs.comicconf as ComicConf
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.gui2.dialogs.book_info import BookInfo
from calibre.ebooks.metadata.meta import set_metadata
@@ -173,11 +174,13 @@ class Main(MainWindow, Ui_MainWindow):
cm.addAction(_('Convert individually'))
cm.addAction(_('Bulk convert'))
cm.addSeparator()
- cm.addAction(_('Set conversion defaults'))
+ cm.addAction(_('Set defaults for conversion to LRF'))
+ cm.addAction(_('Set defaults for conversion of comics'))
self.action_convert.setMenu(cm)
QObject.connect(cm.actions()[0], SIGNAL('triggered(bool)'), self.convert_single)
QObject.connect(cm.actions()[1], SIGNAL('triggered(bool)'), self.convert_bulk)
QObject.connect(cm.actions()[3], SIGNAL('triggered(bool)'), self.set_conversion_defaults)
+ QObject.connect(cm.actions()[4], SIGNAL('triggered(bool)'), self.set_comic_conversion_defaults)
QObject.connect(self.action_convert, SIGNAL('triggered(bool)'), self.convert_single)
self.convert_menu = cm
self.tool_bar.widgetForAction(self.action_news).setPopupMode(QToolButton.InstantPopup)
@@ -763,12 +766,25 @@ class Main(MainWindow, Ui_MainWindow):
############################### Convert ####################################
- def convert_bulk(self, checked):
- rows = self.library_view.selectionModel().selectedRows()
+ def get_books_for_conversion(self):
+ rows = [r.row() for r in self.library_view.selectionModel().selectedRows()]
if not rows or len(rows) == 0:
d = error_dialog(self, _('Cannot convert'), _('No books selected'))
d.exec_()
- return
+ return [], []
+ comics, others = [], []
+ db = self.library_view.model().db
+ for r in rows:
+ formats = db.formats(r)
+ if not formats: continue
+ formats = formats.lower().split(',')
+ if 'cbr' in formats or 'cbz' in formats:
+ comics.append(r)
+ else:
+ others.append(r)
+ return comics, others
+
+ def convert_bulk_others(self, rows):
d = LRFBulkDialog(self)
d.exec_()
if d.result() != QDialog.Accepted:
@@ -813,34 +829,67 @@ class Main(MainWindow, Ui_MainWindow):
cmdline.append(pt.name)
id = self.job_manager.run_conversion_job(self.book_converted,
'any2lrf', args=[cmdline],
- job_description='Convert book %d of %d'%(i+1, len(rows)))
+ job_description=_('Convert book %d of %d (%s)')%(i+1, len(rows), repr(mi.title)))
self.conversion_jobs[id] = (d.cover_file, pt, of, d.output_format,
self.library_view.model().db.id(row))
-
-
res = []
for row in bad_rows:
title = self.library_view.model().db.title(row)
res.append('
%s'%title)
if res:
- msg = 'Could not convert %d of %d books, because no suitable source format was found.
'%(len(res), len(rows), '\n'.join(res))
- warning_dialog(self, 'Could not convert some books', msg).exec_()
-
+ msg = _('Could not convert %d of %d books, because no suitable source format was found.
')%(len(res), len(rows), '\n'.join(res))
+ warning_dialog(self, _('Could not convert some books'), msg).exec_()
+
+
+ def convert_bulk(self, checked):
+ comics, others = self.get_books_for_conversion()
+ if others:
+ self.convert_bulk_others(others)
+ if comics:
+ opts = ComicConf.get_bulk_conversion_options(self)
+ if opts:
+ for i, row in enumerate(comics):
+ options = opts.copy()
+ mi = self.library_view.model().db.get_metadata(row)
+ if mi.title:
+ options.title = mi.title
+ if mi.authors:
+ opts.author = ','.join(mi.authors)
+ data = None
+ for fmt in ['cbz', 'cbr']:
+ try:
+ data = self.library_view.model().db.format(row, fmt.upper())
+ break
+ except:
+ continue
+
+ pt = PersistentTemporaryFile('.'+fmt.lower())
+ pt.write(data)
+ pt.close()
+ of = PersistentTemporaryFile('.lrf')
+ of.close()
+ setattr(options, 'output', of.name)
+ options.verbose = 1
+ args = [pt.name, options]
+ id = self.job_manager.run_conversion_job(self.book_converted,
+ 'comic2lrf', args=args,
+ job_description=_('Convert comic %d of %d (%s)')%(i+1, len(comics), repr(options.title)))
+ self.conversion_jobs[id] = (None, pt, of, 'lrf',
+ self.library_view.model().db.id(row))
+
def set_conversion_defaults(self, checked):
d = LRFSingleDialog(self, None, None)
d.exec_()
-
- def convert_single(self, checked):
- rows = self.library_view.selectionModel().selectedRows()
- if not rows or len(rows) == 0:
- d = error_dialog(self, _('Cannot convert'), _('No books selected'))
- d.exec_()
+ def set_comic_conversion_defaults(self, checked):
+ ComicConf.set_conversion_defaults(self)
+
+ def convert_single_others(self, rows):
changed = False
- for row in [r.row() for r in rows]:
+ for row in rows:
d = LRFSingleDialog(self, self.library_view.model().db, row)
if d.selected_format:
d.exec_()
@@ -856,7 +905,7 @@ class Main(MainWindow, Ui_MainWindow):
cmdline.append(pt.name)
id = self.job_manager.run_conversion_job(self.book_converted,
'any2lrf', args=[cmdline],
- job_description='Convert book:'+d.title())
+ job_description=_('Convert book: ')+d.title())
self.conversion_jobs[id] = (d.cover_file, pt, of, d.output_format, d.id)
@@ -865,6 +914,48 @@ class Main(MainWindow, Ui_MainWindow):
self.library_view.model().resort(reset=False)
self.library_view.model().research()
+
+ def convert_single(self, checked):
+ comics, others = self.get_books_for_conversion()
+ if others:
+ self.convert_single_others(others)
+ changed = False
+ db = self.library_view.model().db
+ for row in comics:
+ mi = db.get_metadata(row)
+ title = author = _('Unknown')
+ if mi.title:
+ title = mi.title
+ if mi.authors:
+ author = ','.join(mi.authors)
+ defaults = db.conversion_options(db.id(row), 'comic')
+ opts, defaults = ComicConf.get_conversion_options(self, defaults, title, author)
+ if defaults is not None:
+ db.set_conversion_options(db.id(row), 'comic', defaults)
+ if opts is None: continue
+ for fmt in ['cbz', 'cbr']:
+ try:
+ data = db.format(row, fmt.upper())
+ break
+ except:
+ continue
+ pt = PersistentTemporaryFile('.'+fmt)
+ pt.write(data)
+ pt.close()
+ of = PersistentTemporaryFile('.lrf')
+ of.close()
+ opts.output = of.name
+ opts.verbose = 1
+ args = [pt.name, opts]
+ changed = True
+ id = self.job_manager.run_conversion_job(self.book_converted,
+ 'comic2lrf', args=args,
+ job_description=_('Convert comic: ')+opts.title)
+ self.conversion_jobs[id] = (None, pt, of, 'lrf',
+ self.library_view.model().db.id(row))
+ if changed:
+ self.library_view.model().resort(reset=False)
+ self.library_view.model().research()
def book_converted(self, id, description, result, exception, formatted_traceback, log):
of, fmt, book_id = self.conversion_jobs.pop(id)[2:]
diff --git a/src/calibre/library/database.py b/src/calibre/library/database.py
index 0a05f48dc9..e84e9b66e3 100644
--- a/src/calibre/library/database.py
+++ b/src/calibre/library/database.py
@@ -1092,7 +1092,7 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
self.set_tags(id, val.split(','), append=False)
def set_conversion_options(self, id, format, options):
- data = sqlite.Binary(cPickle.dumps(options))
+ data = sqlite.Binary(cPickle.dumps(options, -1))
oid = self.conn.execute('SELECT id FROM conversion_options WHERE book=? AND format=?', (id, format.upper())).fetchone()
if oid:
self.conn.execute('UPDATE conversion_options SET data=? WHERE id=?', (data, oid[0]))
diff --git a/src/calibre/linux.py b/src/calibre/linux.py
index b55f02c15e..efcb49b54c 100644
--- a/src/calibre/linux.py
+++ b/src/calibre/linux.py
@@ -1,8 +1,7 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal '
-import shutil
''' Post installation script for linux '''
-import sys, os, re
+import sys, os, re, shutil
from subprocess import check_call, call
from calibre import __version__, __appname__
diff --git a/src/calibre/linux_installer.py b/src/calibre/linux_installer.py
index ad24508900..d2e19e913a 100644
--- a/src/calibre/linux_installer.py
+++ b/src/calibre/linux_installer.py
@@ -215,11 +215,12 @@ cd $frozen_path
'''
def extract_tarball(tar, destdir):
+ print 'Extracting application files...'
if hasattr(tar, 'read'):
try:
tarfile.open(fileobj=tar, mode='r').extractall(destdir)
except: # tarfile.py on Fedora 9 is buggy
- subprocess.check_call(['tar', 'xvjf', tar.name, '-C', destdir])
+ subprocess.check_call(['tar', 'xjf', tar.name, '-C', destdir])
else:
tarfile.open(tar, 'r').extractall(destdir)
@@ -242,6 +243,7 @@ def do_postinstall(destdir):
try:
os.chdir(destdir)
os.environ['LD_LIBRARY_PATH'] = destdir+':'+os.environ.get('LD_LIBRARY_PATH', '')
+ os.environ['PYTHONPATH'] = destdir
subprocess.call((os.path.join(destdir, 'calibre_postinstall'), '--save-manifest-to', t.name))
finally:
os.chdir(cwd)
diff --git a/src/calibre/parallel.py b/src/calibre/parallel.py
index d46f61c8fd..a26e5e0721 100644
--- a/src/calibre/parallel.py
+++ b/src/calibre/parallel.py
@@ -48,6 +48,9 @@ PARALLEL_FUNCS = {
'render_table' :
('calibre.ebooks.lrf.html.table_as_image', 'do_render', {}, None),
+
+ 'comic2lrf' :
+ ('calibre.ebooks.lrf.comic.convert_from', 'do_convert', {}, 'notification'),
}
diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py
new file mode 100644
index 0000000000..eea9c9749e
--- /dev/null
+++ b/src/calibre/utils/config.py
@@ -0,0 +1,269 @@
+from __future__ import with_statement
+__license__ = 'GPL v3'
+__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
+__docformat__ = 'restructuredtext en'
+
+'''
+Manage application-wide preferences.
+'''
+import os, re, cPickle
+from copy import deepcopy
+from PyQt4.QtCore import QString
+from calibre import iswindows, isosx, OptionParser, ExclusiveFile, LockError
+from collections import defaultdict
+
+if iswindows:
+ from calibre import plugins
+ config_dir = plugins['winutil'][0].special_folder_path(plugins['winutil'][0].CSIDL_APPDATA)
+ if not os.access(config_dir, os.W_OK|os.X_OK):
+ config_dir = os.path.expanduser('~')
+ config_dir = os.path.join(config_dir, 'calibre')
+elif isosx:
+ config_dir = os.path.expanduser('~/Library/Preferences/calibre')
+else:
+ config_dir = os.path.expanduser('~/.config/calibre')
+
+if not os.path.exists(config_dir):
+ os.makedirs(config_dir)
+
+class Option(object):
+
+ def __init__(self, name, switches=[], help='', type=None, choices=None,
+ check=None, group=None, default=None, action=None, metavar=None):
+ if choices:
+ type = 'choice'
+
+ self.name = name
+ self.switches = switches
+ self.help = help.replace('%default', repr(default)) if help else None
+ self.type = type
+ self.choices = choices
+ self.check = check
+ self.group = group
+ self.default = default
+ self.action = action
+ self.metavar = metavar
+
+ def __eq__(self, other):
+ return self.name == getattr(other, 'name', None)
+
+class OptionValues(object):
+
+ def copy(self):
+ return deepcopy(self)
+
+class OptionSet(object):
+
+ OVERRIDE_PAT = re.compile(r'#{3,100} Override Options #{15}(.*?)#{3,100} End Override #{3,100}',
+ re.DOTALL|re.IGNORECASE)
+
+ def __init__(self, description=''):
+ self.description = description
+ self.preferences = []
+ self.group_list = []
+ self.groups = {}
+ self.set_buffer = {}
+
+ def has_option(self, name_or_option_object):
+ if name_or_option_object in self.preferences:
+ return True
+ for p in self.preferences:
+ if p.name == name_or_option_object:
+ return True
+ return False
+
+ def add_group(self, name, description=''):
+ if name in self.group_list:
+ raise ValueError('A group by the name %s already exists in this set'%name)
+ self.groups[name] = description
+ self.group_list.append(name)
+
+ def add_opt(self, name, switches=[], help=None, type=None, choices=None,
+ group=None, default=None, action=None, metavar=None):
+ '''
+ Add an option to this section.
+
+ :param name: The name of this option. Must be a valid Python identifier.
+ Must also be unique in this OptionSet and all its subsets.
+ :param switches: List of command line switches for this option
+ (as supplied to :module:`optparse`). If empty, this
+ option will not be added to the command line parser.
+ :param help: Help text.
+ :param type: Type checking of option values. Supported types are:
+ `None, 'choice', 'complex', 'float', 'int', 'long', 'string'`.
+ :param choices: List of strings or `None`.
+ :param group: Group this option belongs to. You must previously
+ have created this group with a call to :method:`add_group`.
+ :param default: The default value for this option.
+ :param action: The action to pass to optparse. Supported values are:
+ `None, 'count'`. For choices and boolean options,
+ action is automatically set correctly.
+ '''
+ pref = Option(name, switches=switches, help=help, type=type, choices=choices,
+ group=group, default=default, action=action, metavar=None)
+ if group is not None and group not in self.groups.keys():
+ raise ValueError('Group %s has not been added to this section'%group)
+ if pref in self.preferences:
+ raise ValueError('An option with the name %s already exists in this set.'%name)
+ self.preferences.append(pref)
+
+ def option_parser(self, user_defaults=None, usage='', gui_mode=False):
+ parser = OptionParser(usage, gui_mode=gui_mode)
+ groups = defaultdict(lambda : parser)
+ for group, desc in self.groups.items():
+ groups[group] = parser.add_group(group, desc)
+
+ for pref in self.preferences:
+ if not pref.switches:
+ continue
+ g = groups[pref.group]
+ action = pref.action
+ if action is None:
+ action = 'store'
+ if pref.default is True or pref.default is False:
+ action = 'store_' + ('false' if pref.default else 'true')
+ args = dict(
+ dest=pref.name,
+ help=pref.help,
+ metavar=pref.metavar,
+ type=pref.type,
+ choices=pref.choices,
+ default=getattr(user_defaults, pref.name, pref.default),
+ action=action,
+ )
+ g.add_option(*pref.switches, **args)
+
+
+ return parser
+
+ def get_override_section(self, src):
+ match = self.OVERRIDE_PAT.search(src)
+ if match:
+ return match.group()
+ return ''
+
+ def parse_string(self, src):
+ options = {'cPickle':cPickle}
+ if src is not None:
+ exec src in options
+ opts = OptionValues()
+ for pref in self.preferences:
+ setattr(opts, pref.name, options.get(pref.name, pref.default))
+
+ return opts
+
+ def render_group(self, name, desc, opts):
+ prefs = [pref for pref in self.preferences if pref.group == name]
+ lines = ['### Begin group: %s'%(name if name else 'DEFAULT')]
+ if desc:
+ lines += map(lambda x: '# '+x for x in desc.split('\n'))
+ lines.append(' ')
+ for pref in prefs:
+ lines.append('# '+pref.name.replace('_', ' '))
+ if pref.help:
+ lines += map(lambda x: '# ' + x, pref.help.split('\n'))
+ lines.append('%s = %s'%(pref.name,
+ self.serialize_opt(getattr(opts, pref.name, pref.default))))
+ lines.append(' ')
+ return '\n'.join(lines)
+
+ def serialize_opt(self, val):
+ if val is val is True or val is False or val is None or \
+ isinstance(val, (int, float, long, basestring)):
+ return repr(val)
+ if isinstance(val, QString):
+ return repr(unicode(val))
+ pickle = cPickle.dumps(val, -1)
+ return 'cPickle.loads(%s)'%repr(pickle)
+
+ def serialize(self, opts):
+ src = '# %s\n\n'%(self.description.replace('\n', '\n# '))
+ groups = [self.render_group(name, self.groups.get(name, ''), opts) \
+ for name in [None] + self.group_list]
+ return src + '\n\n'.join(groups)
+
+class Config(object):
+
+ def __init__(self, basename, description=''):
+ self.config_file_path = os.path.join(config_dir, basename+'.py')
+ self.option_set = OptionSet(description=description)
+ self.add_opt = self.option_set.add_opt
+ self.add_group = self.option_set.add_group
+
+ def option_parser(self, usage='', gui_mode=False):
+ return self.option_set.option_parser(user_defaults=self.parse(),
+ usage=usage, gui_mode=gui_mode)
+
+ def parse(self):
+ try:
+ with ExclusiveFile(self.config_file_path) as f:
+ src = f.read()
+ except LockError:
+ raise IOError('Could not lock config file: %s'%self.config_file_path)
+ return self.option_set.parse_string(src)
+
+ def set(self, name, val):
+ if not self.option_set.has_option(name):
+ raise ValueError('The option %s is not defined.'%name)
+ try:
+ with ExclusiveFile(self.config_file_path) as f:
+ src = f.read()
+ opts = self.option_set.parse_string(src)
+ setattr(opts, name, val)
+ footer = self.option_set.get_override_section(src)
+ src = self.option_set.serialize(opts)+ '\n\n' + footer + '\n'
+ f.seek(0)
+ f.truncate()
+ f.write(src)
+ except LockError:
+ raise IOError('Could not lock config file: %s'%self.config_file_path)
+
+class StringConfig(object):
+
+ def __init__(self, src, description=''):
+ self.src = src
+ self.option_set = OptionSet(description=description)
+ self.add_opt = self.option_set.add_opt
+ self.option_parser = self.option_set.option_parser
+
+ def option_parser(self, usage='', gui_mode=False):
+ return self.option_set.option_parser(user_defaults=self.parse(),
+ usage=usage, gui_mode=gui_mode)
+
+ def parse(self):
+ return self.option_set.parse_string(self.src)
+
+ def set(self, name, val):
+ if not self.option_set.has_option(name):
+ raise ValueError('The option %s is not defined.'%name)
+ opts = self.option_set.parse_string(self.src)
+ setattr(opts, name, val)
+ footer = self.option_set.get_override_section(self.src)
+ self.src = self.option_set.serialize(opts)+ '\n\n' + footer + '\n'
+
+
+
+
+if __name__ == '__main__':
+ import subprocess
+ from PyQt4.Qt import QByteArray
+ c = Config('test', 'test config')
+
+ c.add_opt('one', ['-1', '--one'], help="This is option #1")
+ c.set('one', u'345')
+
+ c.add_opt('two', help="This is option #2")
+ c.set('two', 345)
+
+ c.add_opt('three', help="This is option #3")
+ c.set('three', QString(u'aflatoon'))
+
+ c.add_opt('four', help="This is option #4")
+ c.set('four', QByteArray('binary aflatoon'))
+
+ subprocess.call(['pygmentize', os.path.expanduser('~/.config/calibre/test.py')])
+
+ opts = c.parse()
+ for i in ('one', 'two', 'three', 'four'):
+ print i, repr(getattr(opts, i))
+
\ No newline at end of file