mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Command line PDF conversion and bookmark manager for ebook-viewer
This commit is contained in:
commit
3aa901df7e
@ -195,5 +195,8 @@ class EbookIterator(object):
|
|||||||
self.bookmarks.append(bm)
|
self.bookmarks.append(bm)
|
||||||
self.save_bookmarks()
|
self.save_bookmarks()
|
||||||
|
|
||||||
|
def set_bookmarks(self, bookmarks):
|
||||||
|
self.bookmarks = bookmarks
|
||||||
|
|
||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
self._tdir.__exit__(*args)
|
self._tdir.__exit__(*args)
|
@ -1,16 +1,37 @@
|
|||||||
__license__ = 'GPL v3'
|
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|
||||||
'''Read meta information from PDF files'''
|
'''Read meta information from PDF files'''
|
||||||
|
|
||||||
import sys, re
|
from __future__ import with_statement
|
||||||
|
|
||||||
from calibre.ebooks.metadata import MetaInformation, authors_to_string
|
__license__ = 'GPL v3'
|
||||||
from pyPdf import PdfFileReader
|
__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 """
|
""" Return metadata as a L{MetaInfo} object """
|
||||||
mi = MetaInformation(_('Unknown'), [_('Unknown')])
|
mi = MetaInformation(_('Unknown'), [_('Unknown')])
|
||||||
stream.seek(0)
|
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:
|
try:
|
||||||
info = PdfFileReader(stream).getDocumentInfo()
|
info = PdfFileReader(stream).getDocumentInfo()
|
||||||
if info.title:
|
if info.title:
|
||||||
@ -45,3 +66,68 @@ def set_metadata(stream, mi):
|
|||||||
stream.write(raw)
|
stream.write(raw)
|
||||||
stream.seek(0)
|
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())
|
||||||
|
69
src/calibre/ebooks/pdf/from_any.py
Normal file
69
src/calibre/ebooks/pdf/from_any.py
Normal 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())
|
150
src/calibre/ebooks/pdf/writer.py
Normal file
150
src/calibre/ebooks/pdf/writer.py
Normal 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())
|
||||||
|
|
14298
src/calibre/gui2/images/print-preview.svg
Normal file
14298
src/calibre/gui2/images/print-preview.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 354 KiB |
14229
src/calibre/gui2/images/print.svg
Normal file
14229
src/calibre/gui2/images/print.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 350 KiB |
93
src/calibre/gui2/viewer/bookmarkmanager.py
Normal file
93
src/calibre/gui2/viewer/bookmarkmanager.py
Normal 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()
|
||||||
|
|
57
src/calibre/gui2/viewer/bookmarkmanager.ui
Normal file
57
src/calibre/gui2/viewer/bookmarkmanager.ui
Normal 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>
|
@ -12,6 +12,8 @@ from PyQt4.Qt import QMovie, QApplication, Qt, QIcon, QTimer, QWidget, SIGNAL, \
|
|||||||
QToolButton, QMenu, QInputDialog, QAction
|
QToolButton, QMenu, QInputDialog, QAction
|
||||||
|
|
||||||
from calibre.gui2.viewer.main_ui import Ui_EbookViewer
|
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.main_window import MainWindow
|
||||||
from calibre.gui2 import Application, ORG_NAME, APP_UID, choose_files, \
|
from calibre.gui2 import Application, ORG_NAME, APP_UID, choose_files, \
|
||||||
info_dialog, error_dialog
|
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.toc, SIGNAL('clicked(QModelIndex)'), self.toc_clicked)
|
||||||
self.connect(self.reference, SIGNAL('goto(PyQt_PyObject)'), self.goto)
|
self.connect(self.reference, SIGNAL('goto(PyQt_PyObject)'), self.goto)
|
||||||
|
|
||||||
|
|
||||||
|
self.bookmarks_menu = QMenu()
|
||||||
|
self.action_bookmark.setMenu(self.bookmarks_menu)
|
||||||
self.set_bookmarks([])
|
self.set_bookmarks([])
|
||||||
|
|
||||||
if pathtoebook is not None:
|
if pathtoebook is not None:
|
||||||
f = functools.partial(self.load_ebook, pathtoebook)
|
f = functools.partial(self.load_ebook, pathtoebook)
|
||||||
QTimer.singleShot(50, f)
|
QTimer.singleShot(50, f)
|
||||||
@ -274,6 +280,16 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
self.tool_bar.widgetForAction(self.action_bookmark).setPopupMode(QToolButton.MenuButtonPopup)
|
self.tool_bar.widgetForAction(self.action_bookmark).setPopupMode(QToolButton.MenuButtonPopup)
|
||||||
self.action_full_screen.setCheckable(True)
|
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):
|
def toggle_fullscreen(self, x):
|
||||||
if self.isFullScreen():
|
if self.isFullScreen():
|
||||||
self.showNormal()
|
self.showNormal()
|
||||||
@ -477,17 +493,28 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
self.setCursor(Qt.BusyCursor)
|
self.setCursor(Qt.BusyCursor)
|
||||||
|
|
||||||
def set_bookmarks(self, bookmarks):
|
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
|
current_page = None
|
||||||
for bm in bookmarks:
|
for bm in bookmarks:
|
||||||
if bm[0] == 'calibre_current_page_bookmark':
|
if bm[0] == 'calibre_current_page_bookmark':
|
||||||
current_page = bm
|
current_page = bm
|
||||||
else:
|
else:
|
||||||
menu.addAction(bm[0], partial(self.goto_bookmark, bm))
|
self.bookmarks_menu.addAction(bm[0], partial(self.goto_bookmark, bm))
|
||||||
self.action_bookmark.setMenu(menu)
|
|
||||||
self._menu = menu
|
|
||||||
return current_page
|
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):
|
def save_current_position(self):
|
||||||
try:
|
try:
|
||||||
pos = self.view.bookmark()
|
pos = self.view.bookmark()
|
||||||
|
@ -27,8 +27,8 @@
|
|||||||
<widget class="QWidget" name="layoutWidget" >
|
<widget class="QWidget" name="layoutWidget" >
|
||||||
<layout class="QGridLayout" name="gridLayout" >
|
<layout class="QGridLayout" name="gridLayout" >
|
||||||
<item row="0" column="0" >
|
<item row="0" column="0" >
|
||||||
<widget class="QWebView" native="1" name="view" >
|
<widget class="QWebView" name="view" >
|
||||||
<property name="url" stdset="0" >
|
<property name="url" >
|
||||||
<url>
|
<url>
|
||||||
<string>about:blank</string>
|
<string>about:blank</string>
|
||||||
</url>
|
</url>
|
||||||
@ -89,6 +89,8 @@
|
|||||||
<addaction name="separator" />
|
<addaction name="separator" />
|
||||||
<addaction name="action_preferences" />
|
<addaction name="action_preferences" />
|
||||||
<addaction name="action_full_screen" />
|
<addaction name="action_full_screen" />
|
||||||
|
<addaction name="separator" />
|
||||||
|
<addaction name="action_print" />
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QToolBar" name="tool_bar2" >
|
<widget class="QToolBar" name="tool_bar2" >
|
||||||
<attribute name="toolBarArea" >
|
<attribute name="toolBarArea" >
|
||||||
@ -234,6 +236,15 @@
|
|||||||
<string>Toggle full screen</string>
|
<string>Toggle full screen</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</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>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
|
129
src/calibre/gui2/viewer/printing.py
Normal file
129
src/calibre/gui2/viewer/printing.py
Normal 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())
|
||||||
|
|
@ -60,6 +60,7 @@ entry_points = {
|
|||||||
'calibre-parallel = calibre.parallel:main',
|
'calibre-parallel = calibre.parallel:main',
|
||||||
'calibre-customize = calibre.customize.ui:main',
|
'calibre-customize = calibre.customize.ui:main',
|
||||||
'pdftrim = calibre.ebooks.pdf.pdftrim:main' ,
|
'pdftrim = calibre.ebooks.pdf.pdftrim:main' ,
|
||||||
|
'any2pdf = calibre.ebooks.pdf.from_any:main',
|
||||||
],
|
],
|
||||||
'gui_scripts' : [
|
'gui_scripts' : [
|
||||||
__appname__+' = calibre.gui2.main:main',
|
__appname__+' = calibre.gui2.main:main',
|
||||||
|
@ -71,6 +71,9 @@ PARALLEL_FUNCS = {
|
|||||||
'any2mobi' :
|
'any2mobi' :
|
||||||
('calibre.ebooks.mobi.from_any', 'any2mobi', {}, None),
|
('calibre.ebooks.mobi.from_any', 'any2mobi', {}, None),
|
||||||
|
|
||||||
|
'any2pdf' :
|
||||||
|
('calibre.ebooks.pdf.from_any', 'any2pdf', {}, None),
|
||||||
|
|
||||||
'feeds2mobi' :
|
'feeds2mobi' :
|
||||||
('calibre.ebooks.mobi.from_feeds', 'main', {}, 'notification'),
|
('calibre.ebooks.mobi.from_feeds', 'main', {}, 'notification'),
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user