Framework for GUI conversion dialog

This commit is contained in:
Kovid Goyal 2009-05-03 23:29:34 -07:00
parent 4afc1a7106
commit b090666b36
17 changed files with 1414 additions and 219 deletions

View File

@ -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())

View File

@ -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)

View File

@ -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

View File

@ -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 <llasram@gmail.com>'
__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())

View File

@ -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())

View File

@ -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())

View File

@ -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

View File

@ -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 <kovid@kovidgoyal.net>'
__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('<', '&lt;').replace('>', '&gt;'))
g.setWhatsThis(help.replace('<', '&lt;').replace('>', '&gt;'))
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

View File

@ -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 <kovid@kovidgoyal.net>'
__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)

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="opt_dont_split_on_page_breaks">
<property name="text">
<string>Do not &amp;split on page breaks</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>262</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -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 <kovid@kovidgoyal.net>'
__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)

View File

@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>500</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout_6">
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_18">
<property name="text">
<string>Base &amp;font size:</string>
</property>
<property name="buddy">
<cstring>opt_base_font_size</cstring>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QDoubleSpinBox" name="opt_base_font_size">
<property name="suffix">
<string> pt</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>30.000000000000000</double>
</property>
<property name="singleStep">
<double>1.000000000000000</double>
</property>
<property name="value">
<double>15.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Line &amp;height:</string>
</property>
<property name="buddy">
<cstring>opt_line_height</cstring>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QDoubleSpinBox" name="opt_line_height">
<property name="suffix">
<string> pt</string>
</property>
<property name="decimals">
<number>1</number>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="opt_remove_paragraph_spacing">
<property name="text">
<string>Remove &amp;spacing between paragraphs</string>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="opt_dont_justify">
<property name="text">
<string>No text &amp;justification</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="opt_linearize_tables">
<property name="text">
<string>&amp;Linearize tables</string>
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="opt_remove_first_image">
<property name="text">
<string>Remove &amp;first image from source file</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Font size &amp;key:</string>
</property>
<property name="buddy">
<cstring>opt_font_size_mapping</cstring>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QLineEdit" name="opt_font_size_mapping"/>
</item>
<item row="7" column="0" colspan="2">
<widget class="QCheckBox" name="opt_insert_metadata">
<property name="text">
<string>Insert &amp;metadata at start of book</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Extra &amp;CSS</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QTextEdit" name="opt_extra_css"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../work/calibre-pluginize/src/calibre/gui2/images.qrc"/>
<include location="../images.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -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 <kovid@kovidgoyal.net>'
__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'),
_("<p>There was an error reading from file: <br /><b>") + _file + "</b></p><br />"+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

View File

@ -0,0 +1,336 @@
<ui version="4.0" >
<class>Form</class>
<widget class="QWidget" name="Form" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>500</height>
</rect>
</property>
<property name="windowTitle" >
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout" >
<item>
<widget class="QGroupBox" name="groupBox_4" >
<property name="title" >
<string>Book Cover</string>
</property>
<layout class="QGridLayout" name="_2" >
<item row="1" column="0" >
<layout class="QVBoxLayout" name="_4" >
<property name="spacing" >
<number>6</number>
</property>
<property name="margin" >
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_5" >
<property name="text" >
<string>Change &amp;cover image:</string>
</property>
<property name="buddy" >
<cstring>cover_path</cstring>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="_5" >
<property name="spacing" >
<number>6</number>
</property>
<property name="margin" >
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="cover_path" >
<property name="readOnly" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="cover_button" >
<property name="toolTip" >
<string>Browse for an image to use as the cover of this book.</string>
</property>
<property name="text" >
<string>...</string>
</property>
<property name="icon" >
<iconset resource="../../../../../../calibre/gui2/images.qrc" >
<normaloff>:/images/document_open.svg</normaloff>:/images/document_open.svg</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item row="2" column="0" >
<widget class="QCheckBox" name="opt_prefer_metadata_cover" >
<property name="text" >
<string>Use cover from &amp;source file</string>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0" >
<layout class="QHBoxLayout" name="_3" >
<item>
<widget class="ImageView" name="cover" >
<property name="text" >
<string/>
</property>
<property name="pixmap" >
<pixmap resource="../../../../../../calibre/gui2/images.qrc" >:/images/book.svg</pixmap>
</property>
<property name="scaledContents" >
<bool>true</bool>
</property>
<property name="alignment" >
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
<zorder>opt_prefer_metadata_cover</zorder>
<zorder></zorder>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2" >
<item>
<layout class="QGridLayout" name="_7" >
<item row="0" column="0" >
<widget class="QLabel" name="label" >
<property name="text" >
<string>&amp;Title: </string>
</property>
<property name="alignment" >
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy" >
<cstring>title</cstring>
</property>
</widget>
</item>
<item row="0" column="1" >
<widget class="QLineEdit" name="title" >
<property name="toolTip" >
<string>Change the title of this book</string>
</property>
</widget>
</item>
<item row="1" column="0" >
<widget class="QLabel" name="label_2" >
<property name="text" >
<string>&amp;Author(s): </string>
</property>
<property name="alignment" >
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy" >
<cstring>author</cstring>
</property>
</widget>
</item>
<item row="1" column="1" >
<widget class="QLineEdit" name="author" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip" >
<string>Change the author(s) of this book. Multiple authors should be separated by an &amp;. If the author name contains an &amp;, use &amp;&amp; to represent it.</string>
</property>
</widget>
</item>
<item row="2" column="0" >
<widget class="QLabel" name="label_6" >
<property name="text" >
<string>Author So&amp;rt:</string>
</property>
<property name="alignment" >
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy" >
<cstring>author_sort</cstring>
</property>
</widget>
</item>
<item row="2" column="1" >
<widget class="QLineEdit" name="author_sort" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip" >
<string>Change the author(s) of this book. Multiple authors should be separated by a comma</string>
</property>
</widget>
</item>
<item row="3" column="0" >
<widget class="QLabel" name="label_3" >
<property name="text" >
<string>&amp;Publisher: </string>
</property>
<property name="alignment" >
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy" >
<cstring>publisher</cstring>
</property>
</widget>
</item>
<item row="3" column="1" >
<widget class="QLineEdit" name="publisher" >
<property name="toolTip" >
<string>Change the publisher of this book</string>
</property>
</widget>
</item>
<item row="4" column="0" >
<widget class="QLabel" name="label_4" >
<property name="text" >
<string>Ta&amp;gs: </string>
</property>
<property name="alignment" >
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy" >
<cstring>tags</cstring>
</property>
</widget>
</item>
<item row="4" column="1" >
<widget class="QLineEdit" name="tags" >
<property name="toolTip" >
<string>Tags categorize the book. This is particularly useful while searching. &lt;br>&lt;br>They can be any words or phrases, separated by commas.</string>
</property>
</widget>
</item>
<item row="5" column="0" >
<widget class="QLabel" name="label_7" >
<property name="text" >
<string>&amp;Series:</string>
</property>
<property name="textFormat" >
<enum>Qt::PlainText</enum>
</property>
<property name="alignment" >
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy" >
<cstring>series</cstring>
</property>
</widget>
</item>
<item row="5" column="1" >
<widget class="QComboBox" name="series" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Fixed" hsizetype="Preferred" >
<horstretch>10</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip" >
<string>List of known series. You can add new series.</string>
</property>
<property name="whatsThis" >
<string>List of known series. You can add new series.</string>
</property>
<property name="editable" >
<bool>true</bool>
</property>
<property name="insertPolicy" >
<enum>QComboBox::InsertAlphabetically</enum>
</property>
<property name="sizeAdjustPolicy" >
<enum>QComboBox::AdjustToContents</enum>
</property>
</widget>
</item>
<item row="6" column="1" >
<widget class="QSpinBox" name="series_index" >
<property name="enabled" >
<bool>true</bool>
</property>
<property name="toolTip" >
<string>Series index.</string>
</property>
<property name="whatsThis" >
<string>Series index.</string>
</property>
<property name="prefix" >
<string>Book </string>
</property>
<property name="minimum" >
<number>1</number>
</property>
<property name="maximum" >
<number>10000</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Minimum" hsizetype="Minimum" >
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>200</height>
</size>
</property>
<property name="title" >
<string>Comments</string>
</property>
<layout class="QGridLayout" name="_8" >
<item row="0" column="0" >
<widget class="QTextEdit" name="comment" >
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>180</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ImageView</class>
<extends>QLabel</extends>
<header>widgets.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../../../../../../calibre/gui2/images.qrc" />
<include location="../images.qrc" />
</resources>
<connections/>
</ui>

View File

@ -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 <kovid@kovidgoyal.net>'
__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_()

View File

@ -0,0 +1,200 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1024</width>
<height>700</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<property name="windowIcon">
<iconset resource="../images.qrc">
<normaloff>:/images/convert.svg</normaloff>:/images/convert.svg</iconset>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>&amp;Input format:</string>
</property>
<property name="buddy">
<cstring>input_formats</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="input_formats"/>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Output format:</string>
</property>
<property name="buddy">
<cstring>output_formats</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="output_formats"/>
</item>
</layout>
</item>
<item row="1" column="0" rowspan="3">
<widget class="QListView" name="groups">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="tabKeyNavigation">
<bool>true</bool>
</property>
<property name="iconSize">
<size>
<width>48</width>
<height>48</height>
</size>
</property>
<property name="spacing">
<number>20</number>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QScrollArea" name="scrollArea">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>4</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>810</width>
<height>492</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QStackedWidget" name="stack">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<widget class="QWidget" name="page"/>
<widget class="QWidget" name="page_2"/>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="3" column="1">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QTextEdit" name="help">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>130</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../images.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -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('\\ '):