Command line PDF conversion and bookmark manager for ebook-viewer

This commit is contained in:
Kovid Goyal 2009-02-25 10:23:34 -08:00
commit 3aa901df7e
13 changed files with 29169 additions and 13 deletions

View File

@ -195,5 +195,8 @@ class EbookIterator(object):
self.bookmarks.append(bm)
self.save_bookmarks()
def set_bookmarks(self, bookmarks):
self.bookmarks = bookmarks
def __exit__(self, *args):
self._tdir.__exit__(*args)

View File

@ -1,16 +1,37 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''Read meta information from PDF files'''
import sys, re
from __future__ import with_statement
from calibre.ebooks.metadata import MetaInformation, authors_to_string
from pyPdf import PdfFileReader
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
def get_metadata(stream):
import sys, os, re, StringIO
from calibre.ebooks.metadata import MetaInformation, authors_to_string, get_parser
from calibre.ptempfile import TemporaryDirectory
from pyPdf import PdfFileReader, PdfFileWriter
import Image
try:
from calibre.utils.PythonMagickWand import \
NewMagickWand, MagickReadImage, MagickSetImageFormat, MagickWriteImage
_imagemagick_loaded = True
except:
_imagemagick_loaded = False
def get_metadata(stream, extract_cover=True):
""" Return metadata as a L{MetaInfo} object """
mi = MetaInformation(_('Unknown'), [_('Unknown')])
stream.seek(0)
if extract_cover and _imagemagick_loaded:
try:
cdata = get_cover(stream)
if cdata is not None:
mi.cover_data = ('jpg', cdata)
except:
import traceback
traceback.print_exc()
try:
info = PdfFileReader(stream).getDocumentInfo()
if info.title:
@ -45,3 +66,68 @@ def set_metadata(stream, mi):
stream.write(raw)
stream.seek(0)
def get_cover(stream):
try:
pdf = PdfFileReader(stream)
output = PdfFileWriter()
if len(pdf.pages) >= 1:
output.addPage(pdf.getPage(0))
with TemporaryDirectory('_pdfmeta') as tdir:
cover_path = os.path.join(tdir, 'cover.pdf')
outputStream = file(cover_path, "wb")
output.write(outputStream)
outputStream.close()
wand = NewMagickWand()
MagickReadImage(wand, cover_path)
MagickSetImageFormat(wand, 'JPEG')
MagickWriteImage(wand, '%s.jpg' % cover_path)
img = Image.open('%s.jpg' % cover_path)
data = StringIO.StringIO()
img.save(data, 'JPEG')
return data.getvalue()
except:
import traceback
traceback.print_exc()
def option_parser():
p = get_parser('pdf')
p.remove_option('--category')
p.remove_option('--comment')
p.add_option('--get-cover', default=False, action='store_true',
help=_('Extract the cover'))
return p
def main(args=sys.argv):
p = option_parser()
opts, args = p.parse_args(args)
with open(os.path.abspath(os.path.expanduser(args[1])), 'r+b') as stream:
mi = get_metadata(stream, extract_cover=opts.get_cover)
changed = False
if opts.title:
mi.title = opts.title
changed = True
if opts.authors:
mi.authors = opts.authors.split(',')
changed = True
if changed:
set_metadata(stream, mi)
print unicode(get_metadata(stream, extract_cover=False)).encode('utf-8')
if mi.cover_data[1] is not None:
cpath = os.path.splitext(os.path.basename(args[1]))[0] + '_cover.jpg'
with open(cpath, 'wb') as f:
f.write(mi.cover_data[1])
print 'Cover saved to', f.name
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -0,0 +1,69 @@
'''
Convert any ebook format to PDF.
'''
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net ' \
'and Marshall T. Vandegrift <llasram@gmail.com>' \
'and John Schember <john@nachtimwald.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.pdf.writer import oeb2pdf, config as pdf_config
def config(defaults=None):
c = common_config(defaults=defaults, name='pdf')
c.remove_opt('profile')
pdfc = pdf_config(defaults=defaults)
c.update(pdfc)
return c
def option_parser(usage=USAGE):
usage = usage % ('PDF', formats())
parser = config().option_parser(usage=usage)
return parser
def any2pdf(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]+'.pdf'
opts.output = os.path.abspath(opts.output)
orig_output = opts.output
with TemporaryDirectory('_any2pdf') 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 PDF file from EPUB...'))
oeb2pdf(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
any2pdf(opts, args[1])
if __name__ == '__main__':
sys.exit(main())

View File

@ -0,0 +1,150 @@
'''
Write content to PDF.
'''
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
import os, logging, shutil, sys
from calibre import LoggingInterface
from calibre.ebooks.epub.iterator import SpineItem
from calibre.ebooks.metadata.opf2 import OPF
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.customize.ui import run_plugins_on_postprocess
from calibre.utils.config import Config, StringConfig
from PyQt4 import QtCore
from PyQt4.Qt import QUrl, QEventLoop, SIGNAL, QObject, QApplication, QPrinter, \
QMetaObject, Qt
from PyQt4.QtWebKit import QWebView
from pyPdf import PdfFileWriter, PdfFileReader
class PDFWriter(QObject):
def __init__(self):
if QApplication.instance() is None:
QApplication([])
QObject.__init__(self)
self.logger = logging.getLogger('oeb2pdf')
self.loop = QEventLoop()
self.view = QWebView()
self.connect(self.view, SIGNAL('loadFinished(bool)'), self._render_html)
self.render_queue = []
self.combine_queue = []
self.tmp_path = PersistentTemporaryDirectory('_any2pdf_parts')
def dump(self, oebpath, path):
self._delete_tmpdir()
opf = OPF(oebpath, os.path.dirname(oebpath))
self.render_queue = [SpineItem(i.path) for i in opf.spine]
self.combine_queue = []
self.path = path
QMetaObject.invokeMethod(self, "_render_book", Qt.QueuedConnection)
self.loop.exec_()
@QtCore.pyqtSignature('_render_book()')
def _render_book(self):
if len(self.render_queue) == 0:
self._write()
else:
self._render_next()
def _render_next(self):
item = str(self.render_queue.pop(0))
self.combine_queue.append(os.path.join(self.tmp_path, '%s_%i.pdf' % (os.path.basename(item), len(self.combine_queue))))
self.logger.info('Processing %s...' % item)
self.view.load(QUrl(item))
def _render_html(self, ok):
if ok:
item_path = os.path.join(self.tmp_path, '%s_%i.pdf' % (os.path.basename(str(self.view.url().toLocalFile())), len(self.combine_queue) - 1))
self.logger.debug('\tRendering item as %s' % item_path)
printer = QPrinter(QPrinter.HighResolution)
printer.setPageMargins(1, 1, 1, 1, QPrinter.Inch)
printer.setOutputFormat(QPrinter.PdfFormat)
printer.setOutputFileName(item_path)
self.view.print_(printer)
self._render_book()
def _delete_tmpdir(self):
if os.path.exists(self.tmp_path):
shutil.rmtree(self.tmp_path, True)
self.tmp_path = PersistentTemporaryDirectory('_any2pdf_parts')
def _write(self):
self.logger.info('Combining individual PDF parts...')
try:
outPDF = PdfFileWriter()
for item in self.combine_queue:
inputPDF = PdfFileReader(file(item, 'rb'))
for page in inputPDF.pages:
outPDF.addPage(page)
outputStream = file(self.path, 'wb')
outPDF.write(outputStream)
outputStream.close()
finally:
self._delete_tmpdir()
self.loop.exit(0)
def config(defaults=None):
desc = _('Options to control the conversion to PDF')
if defaults is None:
c = Config('pdf', desc)
else:
c = StringConfig(defaults, desc)
pdf = c.add_group('PDF', _('PDF options.'))
return c
def option_parser():
c = config()
parser = c.option_parser(usage='%prog '+_('[options]')+' file.opf')
parser.add_option(
'-o', '--output', default=None,
help=_('Output file. Default is derived from input filename.'))
parser.add_option(
'-v', '--verbose', default=0, action='count',
help=_('Useful for debugging.'))
return parser
def oeb2pdf(opts, inpath):
logger = LoggingInterface(logging.getLogger('oeb2pdf'))
logger.setup_cli_handler(opts.verbose)
outpath = opts.output
if outpath is None:
outpath = os.path.basename(inpath)
outpath = os.path.splitext(outpath)[0] + '.pdf'
writer = PDFWriter()
writer.dump(inpath, outpath)
run_plugins_on_postprocess(outpath, 'pdf')
logger.log_info(_('Output written to ') + outpath)
def main(argv=sys.argv):
parser = option_parser()
opts, args = parser.parse_args(argv[1:])
if len(args) != 1:
parser.print_help()
return 1
inpath = args[0]
retval = oeb2pdf(opts, inpath)
return retval
if __name__ == '__main__':
sys.exit(main())

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 354 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 350 KiB

View File

@ -0,0 +1,93 @@
__license__ = 'GPL v3'
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
from PyQt4.Qt import Qt, QDialog, QAbstractTableModel, QVariant, SIGNAL, \
QModelIndex, QInputDialog, QLineEdit
from calibre.gui2.viewer.bookmarkmanager_ui import Ui_BookmarkManager
from calibre.gui2 import NONE, qstring_to_unicode
class BookmarkManager(QDialog, Ui_BookmarkManager):
def __init__(self, parent, bookmarks):
QDialog.__init__(self, parent)
self.setupUi(self)
self.bookmarks = bookmarks[:]
self.set_bookmarks()
self.connect(self.button_revert, SIGNAL('clicked()'), self.set_bookmarks)
self.connect(self.button_delete, SIGNAL('clicked()'), self.delete_bookmark)
self.connect(self.button_edit, SIGNAL('clicked()'), self.edit_bookmark)
def set_bookmarks(self):
self._model = BookmarkTableModel(self, self.bookmarks)
self.bookmarks_table.setModel(self._model)
def delete_bookmark(self):
indexes = self.bookmarks_table.selectionModel().selectedIndexes()
if indexes != []:
self._model.remove_row(indexes[0].row())
def edit_bookmark(self):
indexes = self.bookmarks_table.selectionModel().selectedIndexes()
if indexes != []:
title, ok = QInputDialog.getText(self, _('Edit bookmark'), _('New title for bookmark:'), QLineEdit.Normal, self._model.data(indexes[0], Qt.DisplayRole).toString())
title = QVariant(unicode(title).strip())
if ok and title:
self._model.setData(indexes[0], title, Qt.EditRole)
def get_bookmarks(self):
return self._model.bookmarks
class BookmarkTableModel(QAbstractTableModel):
headers = [_("Name")]
def __init__(self, parent, bookmarks):
QAbstractTableModel.__init__(self, parent)
self.bookmarks = bookmarks[:]
def rowCount(self, parent):
if parent and parent.isValid():
return 0
return len(self.bookmarks)
def columnCount(self, parent):
if parent and parent.isValid():
return 0
return len(self.headers)
def data(self, index, role):
if role in (Qt.DisplayRole, Qt.EditRole):
ans = self.bookmarks[index.row()][0]
return NONE if ans is None else QVariant(ans)
return NONE
def setData(self, index, value, role):
if role == Qt.EditRole:
self.bookmarks[index.row()] = (qstring_to_unicode(value.toString()).strip(), self.bookmarks[index.row()][1])
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
return True
return False
def flags(self, index):
flags = QAbstractTableModel.flags(self, index)
flags |= Qt.ItemIsEditable
return flags
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return NONE
if orientation == Qt.Horizontal:
return QVariant(self.headers[section])
else:
return QVariant(section+1)
def remove_row(self, row):
self.beginRemoveRows(QModelIndex(), row, row)
del self.bookmarks[row]
self.endRemoveRows()

View File

@ -0,0 +1,57 @@
<ui version="4.0" >
<class>BookmarkManager</class>
<widget class="QDialog" name="BookmarkManager" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle" >
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout" >
<item row="0" column="0" colspan="3" >
<widget class="QTableView" name="bookmarks_table" >
<property name="showDropIndicator" stdset="0" >
<bool>false</bool>
</property>
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="selectionMode" >
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="sortingEnabled" >
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="0" >
<widget class="QPushButton" name="button_revert" >
<property name="text" >
<string>Revert</string>
</property>
</widget>
</item>
<item row="1" column="1" >
<widget class="QPushButton" name="button_delete" >
<property name="text" >
<string>Delete</string>
</property>
</widget>
</item>
<item row="1" column="2" >
<widget class="QPushButton" name="button_edit" >
<property name="text" >
<string>Edit</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -12,6 +12,8 @@ from PyQt4.Qt import QMovie, QApplication, Qt, QIcon, QTimer, QWidget, SIGNAL, \
QToolButton, QMenu, QInputDialog, QAction
from calibre.gui2.viewer.main_ui import Ui_EbookViewer
from calibre.gui2.viewer.printing import Printing
from calibre.gui2.viewer.bookmarkmanager import BookmarkManager
from calibre.gui2.main_window import MainWindow
from calibre.gui2 import Application, ORG_NAME, APP_UID, choose_files, \
info_dialog, error_dialog
@ -262,7 +264,11 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.connect(self.toc, SIGNAL('clicked(QModelIndex)'), self.toc_clicked)
self.connect(self.reference, SIGNAL('goto(PyQt_PyObject)'), self.goto)
self.bookmarks_menu = QMenu()
self.action_bookmark.setMenu(self.bookmarks_menu)
self.set_bookmarks([])
if pathtoebook is not None:
f = functools.partial(self.load_ebook, pathtoebook)
QTimer.singleShot(50, f)
@ -274,6 +280,16 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.tool_bar.widgetForAction(self.action_bookmark).setPopupMode(QToolButton.MenuButtonPopup)
self.action_full_screen.setCheckable(True)
self.print_menu = QMenu()
self.print_menu.addAction(QIcon(':/images/print-preview.svg'), _('Print Preview'))
self.action_print.setMenu(self.print_menu)
self.tool_bar.widgetForAction(self.action_print).setPopupMode(QToolButton.MenuButtonPopup)
self.connect(self.action_print, SIGNAL("triggered(bool)"), partial(self.print_book, preview=False))
self.connect(self.print_menu.actions()[0], SIGNAL("triggered(bool)"), partial(self.print_book, preview=True))
def print_book(self, preview):
Printing(self.iterator.spine, preview)
def toggle_fullscreen(self, x):
if self.isFullScreen():
self.showNormal()
@ -477,17 +493,28 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.setCursor(Qt.BusyCursor)
def set_bookmarks(self, bookmarks):
menu = QMenu()
self.bookmarks_menu.clear()
self.bookmarks_menu.addAction(_("Manage Bookmarks"), self.manage_bookmarks)
self.bookmarks_menu.addSeparator()
current_page = None
for bm in bookmarks:
if bm[0] == 'calibre_current_page_bookmark':
current_page = bm
else:
menu.addAction(bm[0], partial(self.goto_bookmark, bm))
self.action_bookmark.setMenu(menu)
self._menu = menu
self.bookmarks_menu.addAction(bm[0], partial(self.goto_bookmark, bm))
return current_page
def manage_bookmarks(self):
bmm = BookmarkManager(self, self.iterator.bookmarks)
bmm.exec_()
bookmarks = bmm.get_bookmarks()
if bookmarks != self.iterator.bookmarks:
self.iterator.set_bookmarks(bookmarks)
self.iterator.save_bookmarks()
self.set_bookmarks(bookmarks)
def save_current_position(self):
try:
pos = self.view.bookmark()

View File

@ -27,8 +27,8 @@
<widget class="QWidget" name="layoutWidget" >
<layout class="QGridLayout" name="gridLayout" >
<item row="0" column="0" >
<widget class="QWebView" native="1" name="view" >
<property name="url" stdset="0" >
<widget class="QWebView" name="view" >
<property name="url" >
<url>
<string>about:blank</string>
</url>
@ -89,6 +89,8 @@
<addaction name="separator" />
<addaction name="action_preferences" />
<addaction name="action_full_screen" />
<addaction name="separator" />
<addaction name="action_print" />
</widget>
<widget class="QToolBar" name="tool_bar2" >
<attribute name="toolBarArea" >
@ -234,6 +236,15 @@
<string>Toggle full screen</string>
</property>
</action>
<action name="action_print" >
<property name="icon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/print.svg</normaloff>:/images/print.svg</iconset>
</property>
<property name="text" >
<string>Print</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View File

@ -0,0 +1,129 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
import os, sys, traceback, urlparse
from BeautifulSoup import BeautifulSoup, Tag
from calibre.ebooks.epub.iterator import EbookIterator
from calibre.ptempfile import TemporaryDirectory
from PyQt4 import QtCore
from PyQt4.Qt import QUrl, QEventLoop, SIGNAL, QObject, QApplication, Qt, \
QPrinter, QPrintPreviewDialog, QPrintDialog, QDialog, QMetaObject, Q_ARG
from PyQt4 import QtCore
from PyQt4.QtWebKit import QWebView
PRINTCSS = 'body{width:100%;margin:0;padding:0;font-family:Arial;color:#000;background:none;font-size:12pt;text-align:left;}h1,h2,h3,h4,h5,h6{font-family:Helvetica;}h1{font-size:19pt;}h2{font-size:17pt;}h3{font-size:15pt;}h4,h5,h6{font-size:12pt;}pre,code,samp{font:10ptCourier,monospace;white-space:pre-wrap;page-break-inside:avoid;}blockquote{margin:1.3em;padding:1em;font-size:10pt;}hr{background-color:#ccc;}aimg{border:none;}a:link,a:visited{background:transparent;font-weight:700;text-decoration:underline;color:#333;}a:link:after,a{color:#000;}table{margin:1px;text-align:left;}th{border-bottom:1pxsolid#333;font-weight:bold;}td{border-bottom:1pxsolid#333;}th,td{padding:4px10px4px0;}tfoot{font-style:italic;}caption{background:#fff;margin-bottom:2em;text-align:left;}thead{display:table-header-group;}tr{page-break-inside:avoid;}#header,.header,#footer,.footer,#navbar,.navbar,#navigation,.navigation,#rightSideBar,.rightSideBar,#leftSideBar,.leftSideBar{display:none;}'
class Printing(QObject):
def __init__(self, spine, preview):
if QApplication.instance() is None:
QApplication([])
QObject.__init__(self)
self.loop = QEventLoop()
self.view = QWebView()
if preview:
self.connect(self.view, SIGNAL('loadFinished(bool)'), self.print_preview)
else:
self.connect(self.view, SIGNAL('loadFinished(bool)'), self.print_book)
self.process_content(spine)
def process_content(self, spine):
content = ''
for path in spine:
raw = self.raw_content(path)
content += self.parsed_content(raw, path)
refined_content = self.refine_content(content)
base = os.path.splitdrive(spine[0])[0]
base = base if base != '' else '/'
QMetaObject.invokeMethod(self, "load_content", Qt.QueuedConnection, Q_ARG('QString', refined_content), Q_ARG('QString', base))
self.loop.exec_()
@QtCore.pyqtSignature('load_content(QString, QString)')
def load_content(self, content, base):
self.view.setHtml(content, QUrl(base))
def raw_content(self, path):
return open(path, 'rb').read().decode(path.encoding)
def parsed_content(self, raw_content, path):
dom_tree = BeautifulSoup(raw_content).body
# Remove sytle information that is applied to the entire document.
# This does not remove styles applied within a tag.
styles = dom_tree.findAll('style')
for s in styles:
s.extract()
scripts = dom_tree.findAll('script')
for s in scripts:
s.extract()
# Convert all relative links to absolute paths.
links = dom_tree.findAll(src=True)
for s in links:
if QUrl(s['src']).isRelative():
s['src'] = urlparse.urljoin(path, s['src'])
links = dom_tree.findAll(href=True)
for s in links:
if QUrl(s['href']).isRelative():
s['href'] = urlparse.urljoin(path, s['href'])
return unicode(dom_tree)
# Adds the begenning and endings tags to the document.
# Adds the print css.
def refine_content(self, content):
dom_tree = BeautifulSoup('<html><head></head><body>%s</body></html>' % content)
css = dom_tree.findAll('link')
for c in css:
c.extract()
print_css = Tag(BeautifulSoup(), 'style', [('type', 'text/css'), ('title', 'override_css')])
print_css.insert(0, PRINTCSS)
dom_tree.findAll('head')[0].insert(0, print_css)
return unicode(dom_tree)
def print_preview(self, ok):
printer = QPrinter(QPrinter.HighResolution)
printer.setPageMargins(1, 1, 1, 1, QPrinter.Inch)
previewDialog = QPrintPreviewDialog(printer)
self.connect(previewDialog, SIGNAL('paintRequested(QPrinter *)'), self.view.print_)
previewDialog.exec_()
self.disconnect(previewDialog, SIGNAL('paintRequested(QPrinter *)'), self.view.print_)
self.loop.quit()
def print_book(self, ok):
printer = QPrinter(QPrinter.HighResolution)
printer.setPageMargins(1, 1, 1, 1, QPrinter.Inch)
printDialog = QPrintDialog(printer)
printDialog.setWindowTitle(_("Print eBook"))
printDialog.exec_()
if printDialog.result() == QDialog.Accepted:
self.view.print_(printer)
self.loop.quit()
def main():
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -60,6 +60,7 @@ entry_points = {
'calibre-parallel = calibre.parallel:main',
'calibre-customize = calibre.customize.ui:main',
'pdftrim = calibre.ebooks.pdf.pdftrim:main' ,
'any2pdf = calibre.ebooks.pdf.from_any:main',
],
'gui_scripts' : [
__appname__+' = calibre.gui2.main:main',

View File

@ -71,6 +71,9 @@ PARALLEL_FUNCS = {
'any2mobi' :
('calibre.ebooks.mobi.from_any', 'any2mobi', {}, None),
'any2pdf' :
('calibre.ebooks.pdf.from_any', 'any2pdf', {}, None),
'feeds2mobi' :
('calibre.ebooks.mobi.from_feeds', 'main', {}, 'notification'),