diff --git a/src/calibre/ebooks/epub/iterator.py b/src/calibre/ebooks/epub/iterator.py index e953cbda51..82b9aa09ef 100644 --- a/src/calibre/ebooks/epub/iterator.py +++ b/src/calibre/ebooks/epub/iterator.py @@ -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) \ No newline at end of file + self._tdir.__exit__(*args) diff --git a/src/calibre/ebooks/metadata/pdf.py b/src/calibre/ebooks/metadata/pdf.py index 6f571e2252..8ff652c01b 100644 --- a/src/calibre/ebooks/metadata/pdf.py +++ b/src/calibre/ebooks/metadata/pdf.py @@ -1,16 +1,37 @@ -__license__ = 'GPL v3' -__copyright__ = '2008, Kovid Goyal ' '''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 ' -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()) diff --git a/src/calibre/ebooks/pdf/from_any.py b/src/calibre/ebooks/pdf/from_any.py new file mode 100644 index 0000000000..e4fb937cdb --- /dev/null +++ b/src/calibre/ebooks/pdf/from_any.py @@ -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 ' \ + 'and John Schember ' +__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()) diff --git a/src/calibre/ebooks/pdf/writer.py b/src/calibre/ebooks/pdf/writer.py new file mode 100644 index 0000000000..cfdd3bb336 --- /dev/null +++ b/src/calibre/ebooks/pdf/writer.py @@ -0,0 +1,150 @@ +''' +Write content to PDF. +''' +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, John Schember ' + +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()) + diff --git a/src/calibre/gui2/images/print-preview.svg b/src/calibre/gui2/images/print-preview.svg new file mode 100644 index 0000000000..6ffe4fafa8 --- /dev/null +++ b/src/calibre/gui2/images/print-preview.svg @@ -0,0 +1,14298 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/calibre/gui2/images/print.svg b/src/calibre/gui2/images/print.svg new file mode 100644 index 0000000000..dffa8b94ba --- /dev/null +++ b/src/calibre/gui2/images/print.svg @@ -0,0 +1,14229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/calibre/gui2/viewer/bookmarkmanager.py b/src/calibre/gui2/viewer/bookmarkmanager.py new file mode 100644 index 0000000000..6dcd662754 --- /dev/null +++ b/src/calibre/gui2/viewer/bookmarkmanager.py @@ -0,0 +1,93 @@ +__license__ = 'GPL v3' +__copyright__ = '2009, John Schember ' + + +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() + diff --git a/src/calibre/gui2/viewer/bookmarkmanager.ui b/src/calibre/gui2/viewer/bookmarkmanager.ui new file mode 100644 index 0000000000..44e044b52a --- /dev/null +++ b/src/calibre/gui2/viewer/bookmarkmanager.ui @@ -0,0 +1,57 @@ + + BookmarkManager + + + + 0 + 0 + 400 + 300 + + + + Dialog + + + + + + false + + + true + + + QAbstractItemView::SingleSelection + + + false + + + + + + + Revert + + + + + + + Delete + + + + + + + Edit + + + + + + + + diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index 2c48156e2f..990281a471 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -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) @@ -273,6 +279,16 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.tool_bar2.setContextMenuPolicy(Qt.PreventContextMenu) 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(): @@ -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() diff --git a/src/calibre/gui2/viewer/main.ui b/src/calibre/gui2/viewer/main.ui index 59f813b2bd..c4a571be00 100644 --- a/src/calibre/gui2/viewer/main.ui +++ b/src/calibre/gui2/viewer/main.ui @@ -27,8 +27,8 @@ - - + + about:blank @@ -89,6 +89,8 @@ + + @@ -234,6 +236,15 @@ Toggle full screen + + + + :/images/print.svg:/images/print.svg + + + Print + + diff --git a/src/calibre/gui2/viewer/printing.py b/src/calibre/gui2/viewer/printing.py new file mode 100644 index 0000000000..e948360338 --- /dev/null +++ b/src/calibre/gui2/viewer/printing.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = '2009, John Schember ' + + +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('%s' % 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()) + diff --git a/src/calibre/linux.py b/src/calibre/linux.py index 1fc57ca3a6..d960ef87f7 100644 --- a/src/calibre/linux.py +++ b/src/calibre/linux.py @@ -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', diff --git a/src/calibre/parallel.py b/src/calibre/parallel.py index f9b4513c78..7bdb606642 100644 --- a/src/calibre/parallel.py +++ b/src/calibre/parallel.py @@ -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'),