diff --git a/src/calibre/customize/conversion.py b/src/calibre/customize/conversion.py index 3a89a9b156..4d19ba4fad 100644 --- a/src/calibre/customize/conversion.py +++ b/src/calibre/customize/conversion.py @@ -64,6 +64,10 @@ class OptionRecommendation(object): self.validate_parameters() + @property + def help(self): + return self.option.help + def clone(self): return OptionRecommendation(recommended_value=self.recommended_value, level=self.level, option=self.option.clone()) diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 502102a59a..b409198aac 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -19,6 +19,10 @@ def supported_input_formats(): fmts.add(x) return fmts +INPUT_FORMAT_PREFERENCES = ['cbr', 'cbz', 'cbc', 'lit', 'mobi', 'prc', 'azw', 'fb2', 'html', + 'rtf', 'pdf', 'txt', 'pdb'] +OUTPUT_FORMAT_PREFERENCES = ['epub', 'mobi', 'lit', 'pdf', 'pdb', 'txt'] + class OptionValues(object): pass @@ -114,7 +118,7 @@ OptionRecommendation(name='font_size_mapping', ), OptionRecommendation(name='line_height', - recommended_value=None, level=OptionRecommendation.LOW, + recommended_value=0, level=OptionRecommendation.LOW, help=_('The line height in pts. Controls spacing between consecutive ' 'lines of text. By default no line height manipulation is ' 'performed.' @@ -463,6 +467,12 @@ OptionRecommendation(name='list_recipes', if rec.option == name: return rec + def get_option_help(self, name): + rec = self.get_option_by_name(name) + help = getattr(rec, 'help', None) + if help is not None: + return help.replace('%default', str(rec.recommended_value)) + def merge_plugin_recommendations(self): for source in (self.input_plugin, self.output_plugin): for name, val, level in source.recommendations: @@ -598,7 +608,7 @@ OptionRecommendation(name='list_recipes', from calibre.ebooks.oeb.transforms.flatcss import CSSFlattener fbase = self.opts.base_font_size - if fbase == 0: + if fbase < 1e-4: fbase = float(self.opts.dest.fbase) fkey = self.opts.font_size_mapping if fkey is None: @@ -618,8 +628,11 @@ OptionRecommendation(name='list_recipes', if self.output_plugin.file_type == 'lrf': self.opts.insert_blank_line = False self.opts.remove_paragraph_spacing = False + line_height = self.opts.line_height + if line_height < 1e-4: + line_height = None flattener = CSSFlattener(fbase=fbase, fkey=fkey, - lineh=self.opts.line_height, + lineh=line_height, untable=self.output_plugin.file_type in ('mobi','lit'), unfloat=self.output_plugin.file_type in ('mobi', 'lit')) flattener(self.oeb, self.opts) diff --git a/src/calibre/ebooks/epub/from_comic.py b/src/calibre/ebooks/epub/from_comic.py deleted file mode 100644 index c6dff349da..0000000000 --- a/src/calibre/ebooks/epub/from_comic.py +++ /dev/null @@ -1,21 +0,0 @@ -from __future__ import with_statement -__license__ = 'GPL v3' -__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' -__docformat__ = 'restructuredtext en' - -'Convert a comic in CBR/CBZ format to epub' - -import sys -from functools import partial -from calibre.ebooks.lrf.comic.convert_from import do_convert, option_parser, config, main as _main - -convert = partial(do_convert, output_format='epub') -main = partial(_main, output_format='epub') - -if __name__ == '__main__': - sys.exit(main()) - -if False: - option_parser - config - \ No newline at end of file diff --git a/src/calibre/ebooks/mobi/from_any.py b/src/calibre/ebooks/mobi/from_any.py deleted file mode 100644 index fc9e94dafb..0000000000 --- a/src/calibre/ebooks/mobi/from_any.py +++ /dev/null @@ -1,70 +0,0 @@ -''' -Convert any ebook format to Mobipocket. -''' - -from __future__ import with_statement - -__license__ = 'GPL v3' -__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net ' \ - 'and Marshall T. Vandegrift ' -__docformat__ = 'restructuredtext en' - -import sys, os, glob, logging - -from calibre.ebooks.epub.from_any import any2epub, formats, USAGE -from calibre.ebooks.epub import config as common_config -from calibre.ptempfile import TemporaryDirectory -from calibre.ebooks.mobi.writer import oeb2mobi, config as mobi_config - -def config(defaults=None): - c = common_config(defaults=defaults, name='mobi') - c.remove_opt('profile') - mobic = mobi_config(defaults=defaults) - c.update(mobic) - return c - -def option_parser(usage=USAGE): - usage = usage % ('Mobipocket', formats()) - parser = config().option_parser(usage=usage) - return parser - -def any2mobi(opts, path, notification=None): - ext = os.path.splitext(path)[1] - if not ext: - raise ValueError('Unknown file type: '+path) - ext = ext.lower()[1:] - - if opts.output is None: - opts.output = os.path.splitext(os.path.basename(path))[0]+'.mobi' - - opts.output = os.path.abspath(opts.output) - orig_output = opts.output - - with TemporaryDirectory('_any2mobi') as tdir: - oebdir = os.path.join(tdir, 'oeb') - os.mkdir(oebdir) - opts.output = os.path.join(tdir, 'dummy.epub') - opts.profile = 'None' - opts.dont_split_on_page_breaks = True - orig_bfs = opts.base_font_size2 - opts.base_font_size2 = 0 - any2epub(opts, path, create_epub=False, oeb_cover=True, extract_to=oebdir) - opts.base_font_size2 = orig_bfs - opf = glob.glob(os.path.join(oebdir, '*.opf'))[0] - opts.output = orig_output - logging.getLogger('html2epub').info(_('Creating Mobipocket file from EPUB...')) - oeb2mobi(opts, opf) - - -def main(args=sys.argv): - parser = option_parser() - opts, args = parser.parse_args(args) - if len(args) < 2: - parser.print_help() - print 'No input file specified.' - return 1 - any2mobi(opts, args[1]) - return 0 - -if __name__ == '__main__': - sys.exit(main()) diff --git a/src/calibre/ebooks/mobi/from_comic.py b/src/calibre/ebooks/mobi/from_comic.py deleted file mode 100644 index 87d63ea15f..0000000000 --- a/src/calibre/ebooks/mobi/from_comic.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python -__license__ = 'GPL v3' -__copyright__ = '2009, Kovid Goyal kovid@kovidgoyal.net' -__docformat__ = 'restructuredtext en' - -''' -''' -import sys, os -from calibre.ebooks.lrf.comic.convert_from import do_convert, option_parser, \ - ProgressBar, terminal_controller -from calibre.ebooks.mobi.from_any import config, any2mobi -from calibre.ptempfile import PersistentTemporaryFile - - -def convert(path_to_file, opts, notification=lambda m, p: p): - pt = PersistentTemporaryFile('_comic2mobi.epub') - pt.close() - orig_output = opts.output - opts.output = pt.name - do_convert(path_to_file, opts, notification=notification, output_format='epub') - opts = config('').parse() - if orig_output is None: - orig_output = os.path.splitext(path_to_file)[0]+'.mobi' - opts.output = orig_output - any2mobi(opts, pt.name) - -def main(args=sys.argv): - 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 - - pb = ProgressBar(terminal_controller, _('Rendering comic pages...'), - no_progress_bar=opts.no_progress_bar or getattr(opts, 'no_process', False)) - notification = pb.update - - source = os.path.abspath(args[1]) - convert(source, opts, notification=notification) - return 0 - -if __name__ == '__main__': - sys.exit(main()) \ No newline at end of file diff --git a/src/calibre/ebooks/mobi/from_feeds.py b/src/calibre/ebooks/mobi/from_feeds.py deleted file mode 100644 index 205550f730..0000000000 --- a/src/calibre/ebooks/mobi/from_feeds.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import with_statement -__license__ = 'GPL v3' -__copyright__ = '2009, Kovid Goyal kovid@kovidgoyal.net' -__docformat__ = 'restructuredtext en' - -''' -Convert feeds to MOBI ebook -''' - -import sys, glob, os -from calibre.web.feeds.main import config as feeds2disk_config, USAGE, run_recipe -from calibre.ebooks.mobi.writer import config as oeb2mobi_config, oeb2mobi -from calibre.ptempfile import TemporaryDirectory -from calibre import strftime, sanitize_file_name - -def config(defaults=None): - c = feeds2disk_config(defaults=defaults) - c.remove('lrf') - c.remove('epub') - c.remove('mobi') - c.remove('output_dir') - c.update(oeb2mobi_config(defaults=defaults)) - c.remove('encoding') - c.remove('source_profile') - c.add_opt('output', ['-o', '--output'], default=None, - help=_('Output file. Default is derived from input filename.')) - return c - -def option_parser(): - c = config() - return c.option_parser(usage=USAGE) - -def convert(opts, recipe_arg, notification=None): - opts.lrf = False - opts.epub = False - opts.mobi = True - if opts.debug: - opts.verbose = 2 - parser = option_parser() - with TemporaryDirectory('_feeds2mobi') as tdir: - opts.output_dir = tdir - recipe = run_recipe(opts, recipe_arg, parser, notification=notification) - c = config() - recipe_opts = c.parse_string(recipe.oeb2mobi_options) - c.smart_update(recipe_opts, opts) - opts = recipe_opts - opf = glob.glob(os.path.join(tdir, '*.opf')) - if not opf: - raise Exception('Downloading of recipe: %s failed'%recipe_arg) - opf = opf[0] - - if opts.output is None: - fname = recipe.title + strftime(recipe.timefmt) + '.mobi' - opts.output = os.path.join(os.getcwd(), sanitize_file_name(fname)) - - print 'Generating MOBI...' - opts.encoding = 'utf-8' - opts.source_profile = 'Browser' - oeb2mobi(opts, opf) - - -def main(args=sys.argv, notification=None, handler=None): - parser = option_parser() - opts, args = parser.parse_args(args) - if len(args) != 2 and opts.feeds is None: - parser.print_help() - return 1 - recipe_arg = args[1] if len(args) > 1 else None - convert(opts, recipe_arg, notification=notification) - - return 0 - -if __name__ == '__main__': - sys.exit(main()) \ No newline at end of file diff --git a/src/calibre/ebooks/pdf/output.py b/src/calibre/ebooks/pdf/output.py index ae44d270f7..0cc73931f4 100644 --- a/src/calibre/ebooks/pdf/output.py +++ b/src/calibre/ebooks/pdf/output.py @@ -15,7 +15,6 @@ import os, glob from calibre.customize.conversion import OutputFormatPlugin, \ OptionRecommendation -from calibre.ebooks.oeb.output import OEBOutput from calibre.ebooks.metadata.opf2 import OPF from calibre.ptempfile import TemporaryDirectory from calibre.ebooks.pdf.writer import PDFWriter, ImagePDFWriter, PDFMetadata @@ -52,24 +51,24 @@ class PDFOutput(OutputFormatPlugin): self.input_plugin, self.opts, self.log = input_plugin, opts, log self.output_path = output_path self.metadata = oeb_book.metadata - + if input_plugin.is_image_collection: self.convert_images(input_plugin.get_images()) else: self.convert_text(oeb_book) - + def convert_images(self, images): self.write(ImagePDFWriter, images) - + def convert_text(self, oeb_book): with TemporaryDirectory('_pdf_out') as oeb_dir: from calibre.customize.ui import plugin_for_output_format oeb_output = plugin_for_output_format('oeb') oeb_output.convert(oeb_book, oeb_dir, self.input_plugin, self.opts, self.log) - + opfpath = glob.glob(os.path.join(oeb_dir, '*.opf'))[0] opf = OPF(opfpath, os.path.dirname(opfpath)) - + self.write(PDFWriter, [s.path for s in opf.spine]) def write(self, Writer, items): diff --git a/src/calibre/gui2/convert/__init__.py b/src/calibre/gui2/convert/__init__.py new file mode 100644 index 0000000000..8ecf7f97ab --- /dev/null +++ b/src/calibre/gui2/convert/__init__.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os + +from PyQt4.Qt import QWidget, QSpinBox, QDoubleSpinBox, QLineEdit, QTextEdit, \ + QCheckBox, QComboBox, Qt, QIcon, SIGNAL + +from calibre.customize.conversion import OptionRecommendation +from calibre.utils.config import config_dir +from calibre.utils.lock import ExclusiveFile +from calibre import sanitize_file_name + +config_dir = os.path.join(config_dir, 'conversion') +if not os.path.exists(config_dir): + os.makedirs(config_dir) + +def name_to_path(name): + return os.path.join(config_dir, sanitize_file_name(name)+'.py') + +def save_defaults(name, recs): + path = name_to_path(name) + raw = str(recs) + with open(path, 'wb'): + pass + with ExclusiveFile(path) as f: + f.write(raw) +save_defaults_ = save_defaults + +def load_defaults(name): + path = name_to_path(name) + if not os.path.exists(path): + open(path, 'wb').close() + with ExclusiveFile(path) as f: + raw = f.read() + r = GuiRecommendations() + if raw: + r.from_string(raw) + return r + +def save_specifics(db, book_id, recs): + raw = str(recs) + db.set_conversion_options(book_id, 'PIPE', raw) + +def load_specifics(db, book_id): + raw = db.conversion_options(book_id, 'PIPE') + r = GuiRecommendations() + if raw: + r.from_string(raw) + return r + +class GuiRecommendations(dict): + + def __new__(cls, *args): + dict.__new__(cls) + obj = super(GuiRecommendations, cls).__new__(cls, *args) + obj.disabled_options = set([]) + return obj + + def to_recommendations(self, level=OptionRecommendation.LOW): + ans = [] + for key, val in self.items(): + ans.append((key, val, level)) + return ans + + def __str__(self): + ans = ['{'] + for key, val in self.items(): + ans.append('\t'+repr(key)+' : '+repr(val)+',') + ans.append('}') + return '\n'.join(ans) + + def from_string(self, raw): + try: + d = eval(raw) + except SyntaxError: + d = None + if d: + self.update(d) + + def merge_recommendations(self, get_option, level, options): + for name in options: + opt = get_option(name) + if opt is None: continue + if opt.level == OptionRecommendation.HIGH: + self[name] = opt.recommended_value + self.disabled_options.add(name) + elif opt.level > level or name not in self: + self[name] = opt.recommended_value + + +class Widget(QWidget): + + TITLE = _('Unknown') + ICON = ':/images/config.svg' + HELP = '' + + def __init__(self, parent, name, options): + QWidget.__init__(self, parent) + self.setupUi(self) + self._options = options + self._name = name + self._icon = QIcon(self.ICON) + for name in self._options: + if not hasattr(self, 'opt_'+name): + raise Exception('Option %s missing in %s'%(name, + self.__class__.__name__)) + + def initialize_options(self, get_option, get_help, db=None, book_id=None): + ''' + :param get_option: A callable that takes one argument: the option name + and returns the correspoing OptionRecommendation. + :param get_help: A callable that takes the option name and return a help + string. + ''' + defaults = load_defaults(self._name) + defaults.merge_recommendations(get_option, OptionRecommendation.LOW, + self._options) + + if db is not None: + specifics = load_specifics(db, book_id) + specifics.merge_recommendations(get_option, OptionRecommendation.HIGH, + self._options) + defaults.update(specifics) + self.apply_recommendations(defaults) + self.setup_help(get_help) + + def commit_options(self, save_defaults=False): + recs = self.create_recommendations() + if save_defaults: + save_defaults_(self._name, recs) + return recs + + def create_recommendations(self): + recs = GuiRecommendations() + for name in self._options: + gui_opt = getattr(self, 'opt_'+name, None) + if gui_opt is None: continue + recs[name] = self.get_value(gui_opt) + return recs + + def apply_recommendations(self, recs): + for name, val in recs.items(): + gui_opt = getattr(self, 'opt_'+name, None) + if gui_opt is None: continue + self.set_value(gui_opt, val) + if name in getattr(recs, 'disabled_options', []): + gui_opt.setDisabled(True) + + + def get_value(self, g): + if self.get_value_handler(g): + return + if isinstance(g, (QSpinBox, QDoubleSpinBox)): + return g.value() + elif isinstance(g, (QLineEdit, QTextEdit)): + func = getattr(g, 'toPlainText', getattr(g, 'text', None))() + ans = unicode(func).strip() + if not ans: + ans = None + return ans + elif isinstance(g, QComboBox): + return unicode(g.currentText()) + elif isinstance(g, QCheckBox): + return bool(g.isChecked()) + else: + raise Exception('Can\'t get value from %s'%type(g)) + + + def set_value(self, g, val): + if self.set_value_handler(g, val): + return + if isinstance(g, (QSpinBox, QDoubleSpinBox)): + g.setValue(val) + elif isinstance(g, (QLineEdit, QTextEdit)): + if not val: val = '' + getattr(g, 'setPlainText', g.setText)(val) + getattr(g, 'setCursorPosition', lambda x: x)(0) + elif isinstance(g, QComboBox) and val: + idx = g.findText(val, Qt.MatchFixedString) + if idx < 0: + g.addItem(val) + idx = g.findText(val, Qt.MatchFixedString) + g.setCurrentIndex(idx) + elif isinstance(g, QCheckBox): + g.setCheckState(Qt.Checked if bool(val) else Qt.Unchecked) + else: + raise Exception('Can\'t set value in %s'%type(g)) + self.post_set_value(g, val) + + def set_help(self, msg): + if msg and getattr(msg, 'strip', lambda:True)(): + self.emit(SIGNAL('set_help(PyQt_PyObject)'), msg) + + def setup_help(self, help_provider): + for name in self._options: + g = getattr(self, 'opt_'+name, None) + if g is None: + continue + help = help_provider(name) + if not help: continue + g._help = help + g.setToolTip(help.replace('<', '<').replace('>', '>')) + g.setWhatsThis(help.replace('<', '<').replace('>', '>')) + g.__class__.enterEvent = lambda obj, event: self.set_help(getattr(obj, '_help', obj.toolTip())) + + + def set_value_handler(self, g, val): + return False + + def post_set_value(self, g, val): + pass + + def get_value_handler(self, g): + return False + + def post_get_value(self, g): + pass + + def commit(self, save_defaults=False): + return self.commit_options(save_defaults) + + def config_title(self): + return self.TITLE + + def config_icon(self): + return self._icon + diff --git a/src/calibre/gui2/convert/epub_output.py b/src/calibre/gui2/convert/epub_output.py new file mode 100644 index 0000000000..a8caa23366 --- /dev/null +++ b/src/calibre/gui2/convert/epub_output.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + +from calibre.gui2.convert.epub_output_ui import Ui_Form +from calibre.gui2.convert import Widget + +class PluginWidget(Widget, Ui_Form): + + TITLE = _('EPUB Output') + + def __init__(self, parent, get_option, get_help, db=None, book_id=None): + Widget.__init__(self, parent, 'epub_output', + ['dont_split_on_page_breaks'] + ) + self.db, self.book_id = db, book_id + self.initialize_options(get_option, get_help, db, book_id) + diff --git a/src/calibre/gui2/convert/epub_output.ui b/src/calibre/gui2/convert/epub_output.ui new file mode 100644 index 0000000000..3009e1e5ab --- /dev/null +++ b/src/calibre/gui2/convert/epub_output.ui @@ -0,0 +1,41 @@ + + + Form + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + Do not &split on page breaks + + + + + + + Qt::Vertical + + + + 20 + 262 + + + + + + + + + diff --git a/src/calibre/gui2/convert/look_and_feel.py b/src/calibre/gui2/convert/look_and_feel.py new file mode 100644 index 0000000000..397937dc78 --- /dev/null +++ b/src/calibre/gui2/convert/look_and_feel.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + +from calibre.gui2.convert.look_and_feel_ui import Ui_Form +from calibre.gui2.convert import Widget + +class LookAndFeelWidget(Widget, Ui_Form): + + TITLE = _('Look & Feel') + ICON = ':/images/lookfeel.svg' + HELP = _('Control the look and feel of the output') + + def __init__(self, parent, get_option, get_help, db=None, book_id=None): + Widget.__init__(self, parent, 'look_and_feel', + ['dont_justify', 'extra_css', 'base_font_size', + 'font_size_mapping', 'insert_metadata', 'line_height', + 'linearize_tables', 'remove_first_image', + 'remove_paragraph_spacing'] + ) + self.db, self.book_id = db, book_id + self.initialize_options(get_option, get_help, db, book_id) + diff --git a/src/calibre/gui2/convert/look_and_feel.ui b/src/calibre/gui2/convert/look_and_feel.ui new file mode 100644 index 0000000000..302ca13776 --- /dev/null +++ b/src/calibre/gui2/convert/look_and_feel.ui @@ -0,0 +1,140 @@ + + + Form + + + + 0 + 0 + 600 + 500 + + + + Form + + + + + + + + Base &font size: + + + opt_base_font_size + + + + + + + pt + + + 1 + + + 0.000000000000000 + + + 30.000000000000000 + + + 1.000000000000000 + + + 15.000000000000000 + + + + + + + Line &height: + + + opt_line_height + + + + + + + pt + + + 1 + + + + + + + Remove &spacing between paragraphs + + + + + + + No text &justification + + + + + + + &Linearize tables + + + + + + + Remove &first image from source file + + + + + + + Font size &key: + + + opt_font_size_mapping + + + + + + + + + + Insert &metadata at start of book + + + + + + + + + Extra &CSS + + + + + + + + + + + + + + + + diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py new file mode 100644 index 0000000000..31d8db0867 --- /dev/null +++ b/src/calibre/gui2/convert/metadata.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os, uuid + +from PyQt4.Qt import QPixmap, SIGNAL + +from calibre.gui2 import choose_images, error_dialog, pixmap_to_data +from calibre.gui2.convert.metadata_ui import Ui_Form +from calibre.ebooks.metadata import authors_to_string, string_to_authors, \ + MetaInformation +from calibre.ebooks.metadata.opf2 import OPFCreator +from calibre.ptempfile import PersistentTemporaryFile +from calibre.gui2.convert import Widget + +class MetadataWidget(Widget, Ui_Form): + + TITLE = _('Metadata') + ICON = ':/images/dialog_information.svg' + HELP = _('Set the metadata. The output file will contain as much of this ' + 'metadata as possible.') + + def __init__(self, parent, get_option, get_help, db=None, book_id=None): + Widget.__init__(self, parent, 'metadata', ['prefer_metadata_cover']) + self.db, self.book_id = db, book_id + self.cover_changed = False + if self.db is not None: + self.initialize_metadata_options() + self.initialize_options(get_option, get_help, db, book_id) + self.connect(self.cover_button, SIGNAL("clicked()"), self.select_cover) + + def initialize_metadata_options(self): + all_series = self.db.all_series() + all_series.sort(cmp=lambda x, y : cmp(x[1], y[1])) + for series in all_series: + self.series.addItem(series[1]) + self.series.setCurrentIndex(-1) + + mi = self.db.get_metadata(self.book_id, index_is_id=True) + self.title.setText(mi.title) + if mi.authors: + self.author.setText(authors_to_string(mi.authors)) + else: + self.author.setText('') + self.publisher.setText(mi.publisher if mi.publisher else '') + self.author_sort.setText(mi.author_sort if mi.author_sort else '') + self.tags.setText(', '.join(mi.tags if mi.tags else [])) + self.comment.setText(mi.comments if mi.comments else '') + if mi.series: + self.series.setCurrentIndex(self.series.findText(mi.series)) + if mi.series_index is not None: + self.series_index.setValue(mi.series_index) + + cover = self.db.cover(self.book_id, index_is_id=True) + if cover: + pm = QPixmap() + pm.loadFromData(cover) + if not pm.isNull(): + self.cover.setPixmap(pm) + + def get_title_and_authors(self): + title = unicode(self.title.text()).strip() + if not title: + title = _('Unknown') + authors = unicode(self.author.text()).strip() + authors = string_to_authors(authors) if authors else [_('Unknown')] + return title, authors + + def get_metadata(self): + title, authors = self.get_title_and_authors() + mi = MetaInformation(title, authors) + publisher = unicode(self.publisher.text()).strip() + if publisher: + mi.publisher = publisher + author_sort = unicode(self.author_sort.text()).strip() + if author_sort: + mi.author_sort = author_sort + comments = unicode(self.comment.toPlainText()).strip() + if comments: + mi.comments = comments + mi.series_index = int(self.series_index.value()) + if self.series.currentIndex() > -1: + mi.series = unicode(self.series.currentText()).strip() + tags = [t.strip() for t in unicode(self.tags.text()).strip().split(',')] + if tags: + mi.tags = tags + + return mi + + def select_cover(self): + files = choose_images(self, 'change cover dialog', + _('Choose cover for ') + unicode(self.title.text())) + if not files: + return + _file = files[0] + if _file: + _file = os.path.abspath(_file) + if not os.access(_file, os.R_OK): + d = error_dialog(self.window, _('Cannot read'), + _('You do not have permission to read the file: ') + _file) + d.exec_() + return + cf, cover = None, None + try: + cf = open(_file, "rb") + cover = cf.read() + except IOError, e: + d = error_dialog(self.window, _('Error reading file'), + _("

There was an error reading from file:
") + _file + "


"+str(e)) + d.exec_() + if cover: + pix = QPixmap() + pix.loadFromData(cover) + if pix.isNull(): + d = error_dialog(self.window, _('Error reading file'), + _file + _(" is not a valid picture")) + d.exec_() + else: + self.cover_path.setText(_file) + self.cover.setPixmap(pix) + self.cover_changed = True + self.cpixmap = pix + + def get_recommendations(self): + return { + 'prefer_metadata_cover': + bool(self.opt_prefer_metadata_cover.isChecked()), + } + + + def commit(self, save_defaults=False): + ''' + Settings are stored in two attributes: `opf_file` and `cover_file`. + Both may be None. Also returns a recommendation dictionary. + ''' + recs = self.commit_options(save_defaults) + self.user_mi = self.get_metadata() + self.cover_file = self.opf_file = None + if self.db is not None: + self.db.set_metadata(self.book_id, self.user_mi) + self.mi = self.db.get_metadata(self.book_id, index_is_id=True) + self.mi.application_id = uuid.uuid4() + opf = OPFCreator(os.getcwdu(), self.mi) + self.opf_file = PersistentTemporaryFile('.opf') + opf.render(self.opf_file) + self.opf_file.close() + if self.cover_changed: + self.db.set_cover(self.book_id, pixmap_to_data(self.cover.pixmap())) + cover = self.db.cover(self.book_id, index_is_id=True) + if cover: + cf = PersistentTemporaryFile('.jpeg') + cf.write(cover) + cf.close() + self.cover_file = cf + return recs + diff --git a/src/calibre/gui2/convert/metadata.ui b/src/calibre/gui2/convert/metadata.ui new file mode 100644 index 0000000000..5b68d6383d --- /dev/null +++ b/src/calibre/gui2/convert/metadata.ui @@ -0,0 +1,336 @@ + + Form + + + + 0 + 0 + 600 + 500 + + + + Form + + + + + + Book Cover + + + + + + 6 + + + 0 + + + + + Change &cover image: + + + cover_path + + + + + + + 6 + + + 0 + + + + + true + + + + + + + Browse for an image to use as the cover of this book. + + + ... + + + + :/images/document_open.svg:/images/document_open.svg + + + + + + + + + + + Use cover from &source file + + + true + + + + + + + + + + + + :/images/book.svg + + + true + + + Qt::AlignCenter + + + + + + + opt_prefer_metadata_cover + + + + + + + + + + + &Title: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + title + + + + + + + Change the title of this book + + + + + + + &Author(s): + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + author + + + + + + + + 1 + 0 + + + + Change the author(s) of this book. Multiple authors should be separated by an &. If the author name contains an &, use && to represent it. + + + + + + + Author So&rt: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + author_sort + + + + + + + + 0 + 0 + + + + Change the author(s) of this book. Multiple authors should be separated by a comma + + + + + + + &Publisher: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + publisher + + + + + + + Change the publisher of this book + + + + + + + Ta&gs: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + tags + + + + + + + Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas. + + + + + + + &Series: + + + Qt::PlainText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + series + + + + + + + + 10 + 0 + + + + List of known series. You can add new series. + + + List of known series. You can add new series. + + + true + + + QComboBox::InsertAlphabetically + + + QComboBox::AdjustToContents + + + + + + + true + + + Series index. + + + Series index. + + + Book + + + 1 + + + 10000 + + + + + + + + + + 0 + 0 + + + + + 16777215 + 200 + + + + Comments + + + + + + + 16777215 + 180 + + + + + + + + + + + + + + ImageView + QLabel +
widgets.h
+
+
+ + + + + +
diff --git a/src/calibre/gui2/convert/single.py b/src/calibre/gui2/convert/single.py new file mode 100644 index 0000000000..faa8ebcf83 --- /dev/null +++ b/src/calibre/gui2/convert/single.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import sys, cPickle + +from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont + +from calibre.gui2 import ResizableDialog, NONE +from calibre.gui2.convert import GuiRecommendations, save_specifics +from calibre.gui2.convert.single_ui import Ui_Dialog +from calibre.gui2.convert.metadata import MetadataWidget +from calibre.gui2.convert.look_and_feel import LookAndFeelWidget + +from calibre.ebooks.conversion.plumber import Plumber, supported_input_formats, \ + INPUT_FORMAT_PREFERENCES, OUTPUT_FORMAT_PREFERENCES +from calibre.customize.ui import available_output_formats +from calibre.customize.conversion import OptionRecommendation +from calibre.utils.logging import Log + +class NoSupportedInputFormats(Exception): + pass + +def sort_formats_by_preference(formats, prefs): + def fcmp(x, y): + try: + x = prefs.index(x) + except ValueError: + x = sys.maxint + try: + y = prefs.index(y) + except ValueError: + y = sys.maxint + return cmp(x, y) + return sorted(formats, cmp=fcmp) + +class GroupModel(QAbstractListModel): + + def __init__(self, widgets): + self.widgets = widgets + QAbstractListModel.__init__(self) + + def rowCount(self, *args): + return len(self.widgets) + + def data(self, index, role): + try: + widget = self.widgets[index.row()] + except: + return NONE + if role == Qt.DisplayRole: + return QVariant(widget.config_title()) + if role == Qt.DecorationRole: + return QVariant(widget.config_icon()) + if role == Qt.FontRole: + f = QFont() + f.setBold(True) + return QVariant(f) + return NONE + +class Config(ResizableDialog, Ui_Dialog): + ''' + Configuration dialog for single book conversion. If accepted, has the + following important attributes + + input_path - Path to input file + output_format - Output format (without a leading .) + opf_path - Path to OPF file with user specified metadata + cover_path - Path to user specified cover (can be None) + recommendations - A pickled list of 3 tuples in the same format as the + recommendations member of the Input/Output plugins. + ''' + + def __init__(self, parent, db, book_id, + preferred_input_format=None, preferred_output_format=None): + ResizableDialog.__init__(self, parent) + + self.setup_input_output_formats(db, book_id, preferred_input_format, + preferred_input_format) + self.db, self.book_id = db, book_id + self.setup_pipeline() + + self.connect(self.input_formats, SIGNAL('currentIndexChanged(QString)'), + self.setup_pipeline) + self.connect(self.output_formats, SIGNAL('currentIndexChanged(QString)'), + self.setup_pipeline) + self.connect(self.groups, SIGNAL('activated(QModelIndex)'), + self.show_pane) + self.connect(self.groups, SIGNAL('clicked(QModelIndex)'), + self.show_pane) + self.connect(self.groups, SIGNAL('itemEntered(QModelIndex)'), + self.show_group_help) + self.groups.setMouseTracking(True) + + + def setup_pipeline(self, *args): + input_format = unicode(self.input_formats.currentText()).lower() + output_format = unicode(self.output_formats.currentText()).lower() + input_path = self.db.format_abspath(self.book_id, input_format, + index_is_id=True) + self.input_path = input_path + self.output_format = output_format + output_path = 'dummy.'+output_format + log = Log() + log.outputs = [] + self.plumber = Plumber(input_path, output_path, log) + + def widget_factory(cls): + return cls(self.stack, self.plumber.get_option_by_name, + self.plumber.get_option_help, self.db, self.book_id) + + + self.mw = widget_factory(MetadataWidget) + self.setWindowTitle(_('Convert')+ ' ' + unicode(self.mw.title.text())) + lf = widget_factory(LookAndFeelWidget) + + output_widget = None + name = self.plumber.output_plugin.name.lower().replace(' ', '_') + try: + output_widget = __import__(name) + pw = output_widget.PluginWidget + pw.ICON = ':/images/forward.svg' + pw.HELP = _('Options specific to the output format.') + output_widget = widget_factory(pw) + except ImportError: + pass + input_widget = None + name = self.plumber.input_plugin.name.lower().replace(' ', '_') + try: + input_widget = __import__(name) + pw = input_widget.PluginWidget + pw.ICON = ':/images/forward.svg' + pw.HELP = _('Options specific to the input format.') + input_widget = widget_factory(pw) + except ImportError: + pass + + while True: + c = self.stack.currentWidget() + if not c: break + self.stack.removeWidget(c) + + widgets = [self.mw, lf] + if input_widget is not None: + widgets.append(input_widget) + if output_widget is not None: + widgets.append(output_widget) + for w in widgets: + self.stack.addWidget(w) + self.connect(w, SIGNAL('set_help(PyQt_PyObject)'), + self.help.setPlainText) + + self._groups_model = GroupModel(widgets) + self.groups.setModel(self._groups_model) + + self.groups.setCurrentIndex(self._groups_model.index(0)) + + + def setup_input_output_formats(self, db, book_id, preferred_input_format, + preferred_output_format): + available_formats = db.formats(book_id, index_is_id=True) + if not available_formats: + available_formats = '' + available_formats = set([x.lower() for x in + available_formats.split(',')]) + input_formats = set([x.lower() for x in supported_input_formats()]) + input_formats = \ + sorted(available_formats.intersection(input_formats)) + if not input_formats: + raise NoSupportedInputFormats + output_formats = sorted(available_output_formats()) + output_formats.remove('oeb') + preferred_input_format = preferred_input_format if \ + preferred_input_format in input_formats else \ + sort_formats_by_preference(input_formats, + INPUT_FORMAT_PREFERENCES)[0] + preferred_output_format = preferred_output_format if \ + preferred_output_format in output_formats else \ + sort_formats_by_preference(output_formats, + OUTPUT_FORMAT_PREFERENCES)[0] + self.input_formats.addItems(list(map(QString, [x.upper() for x in + input_formats]))) + self.output_formats.addItems(list(map(QString, [x.upper() for x in + output_formats]))) + self.input_formats.setCurrentIndex(input_formats.index(preferred_input_format)) + self.output_formats.setCurrentIndex(output_formats.index(preferred_output_format)) + + def show_pane(self, index): + self.stack.setCurrentIndex(index.row()) + + def accept(self): + recs = GuiRecommendations() + for w in self._groups_model.widgets: + x = w.commit(save_defaults=False) + recs.update(x) + self.opf_path, self.cover_path = self.mw.opf_file, self.mw.cover_file + self._recommendations = recs + if self.db is not None: + save_specifics(self.db, self.book_id, recs) + ResizableDialog.accept(self) + + @property + def recommendations(self): + recs = [(k, v, OptionRecommendation.HIGH) for k, v in + self._recommendations.items()] + return cPickle.dumps(recs, -1) + + def show_group_help(self, index): + widget = self._groups_model.widgets[index.row()] + self.help.setPlainText(widget.HELP) + + +if __name__ == '__main__': + from calibre.library.database2 import LibraryDatabase2 + from calibre.gui2 import images_rc, Application + images_rc + a=Application([]) + db = LibraryDatabase2('/home/kovid/documents/library') + d = Config(None, db, 998) + d.show() + a.exec_() + diff --git a/src/calibre/gui2/convert/single.ui b/src/calibre/gui2/convert/single.ui new file mode 100644 index 0000000000..713f7471f3 --- /dev/null +++ b/src/calibre/gui2/convert/single.ui @@ -0,0 +1,200 @@ + + + Dialog + + + + 0 + 0 + 1024 + 700 + + + + Dialog + + + + :/images/convert.svg:/images/convert.svg + + + + + + + + &Input format: + + + input_formats + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + &Output format: + + + output_formats + + + + + + + + + + + + + 1 + 0 + + + + true + + + + 48 + 48 + + + + 20 + + + true + + + + + + + + 4 + 10 + + + + QFrame::NoFrame + + + 0 + + + true + + + + + 0 + 0 + 810 + 492 + + + + + 0 + + + + + + 0 + 0 + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + 0 + 0 + + + + + 16777215 + 130 + + + + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/calibre/utils/complete.py b/src/calibre/utils/complete.py index 8fc4fe85e2..7164e61635 100644 --- a/src/calibre/utils/complete.py +++ b/src/calibre/utils/complete.py @@ -53,7 +53,7 @@ def get_opts_from_parser(parser, prefix): for x in do_opt(o): yield x def send(ans): - pat = re.compile('([^0-9a-zA-Z_.])') + pat = re.compile('([^0-9a-zA-Z_./])') for x in sorted(set(ans)): x = pat.sub(lambda m : '\\'+m.group(1), x) if x.endswith('\\ '):