class Page(QWebPage): # {{{ def __init__(self, log): self.log = log QWebPage.__init__(self) self.js = None self.evaljs = self.mainFrame().evaluateJavaScript self.bridge_value = None nam = self.networkAccessManager() nam.setNetworkAccessible(nam.NotAccessible) self.longjs_counter = 0 def javaScriptConsoleMessage(self, msg, lineno, msgid): self.log(u'JS:', unicode(msg)) def javaScriptAlert(self, frame, msg): self.log(unicode(msg)) @pyqtSlot(result=bool) def shouldInterruptJavaScript(self): if self.longjs_counter < 5: self.log('Long running javascript, letting it proceed') self.longjs_counter += 1 return False self.log.warn('Long running javascript, aborting it') return True def _pass_json_value_getter(self): val = json.dumps(self.bridge_value) return QString(val) def _pass_json_value_setter(self, value): self.bridge_value = json.loads(unicode(value)) _pass_json_value = pyqtProperty(QString, fget=_pass_json_value_getter, fset=_pass_json_value_setter) def load_js(self): self.longjs_counter = 0 if self.js is None: from calibre.utils.resources import compiled_coffeescript self.js = compiled_coffeescript('ebooks.oeb.display.utils') self.js += compiled_coffeescript('ebooks.oeb.polish.font_stats') self.mainFrame().addToJavaScriptWindowObject("py_bridge", self) self.evaljs(self.js) self.evaljs(''' py_bridge.__defineGetter__('value', function() { return JSON.parse(this._pass_json_value); }); py_bridge.__defineSetter__('value', function(val) { this._pass_json_value = JSON.stringify(val); }); ''')
class PDFWriter(QObject): def _pass_json_value_getter(self): val = json.dumps(self.bridge_value) return QString(val) def _pass_json_value_setter(self, value): self.bridge_value = json.loads(unicode(value)) _pass_json_value = pyqtProperty(QString, fget=_pass_json_value_getter, fset=_pass_json_value_setter) @pyqtSlot(result=unicode) def title(self): return self.doc_title @pyqtSlot(result=unicode) def author(self): return self.doc_author @pyqtSlot(result=unicode) def section(self): return self.current_section def __init__(self, opts, log, cover_data=None, toc=None): from calibre.gui2 import is_ok_to_use_qt if not is_ok_to_use_qt(): raise Exception('Not OK to use Qt') QObject.__init__(self) self.logger = self.log = log self.opts = opts self.cover_data = cover_data self.paged_js = None self.toc = toc self.loop = QEventLoop() self.view = QWebView() self.page = Page(opts, self.log) self.view.setPage(self.page) self.view.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform) self.view.loadFinished.connect(self.render_html, type=Qt.QueuedConnection) for x in (Qt.Horizontal, Qt.Vertical): self.view.page().mainFrame().setScrollBarPolicy( x, Qt.ScrollBarAlwaysOff) self.report_progress = lambda x, y: x self.current_section = '' def dump(self, items, out_stream, pdf_metadata): opts = self.opts page_size = get_page_size(self.opts) xdpi, ydpi = self.view.logicalDpiX(), self.view.logicalDpiY() # We cannot set the side margins in the webview as there is no right # margin for the last page (the margins are implemented with # -webkit-column-gap) ml, mr = opts.margin_left, opts.margin_right self.doc = PdfDevice(out_stream, page_size=page_size, left_margin=ml, top_margin=0, right_margin=mr, bottom_margin=0, xdpi=xdpi, ydpi=ydpi, errors=self.log.error, debug=self.log.debug, compress=not opts.uncompressed_pdf, mark_links=opts.pdf_mark_links) self.footer = opts.pdf_footer_template if self.footer: self.footer = self.footer.strip() if not self.footer and opts.pdf_page_numbers: self.footer = '<p style="text-align:center; text-indent: 0">_PAGENUM_</p>' self.header = opts.pdf_header_template if self.header: self.header = self.header.strip() min_margin = 36 if self.footer and opts.margin_bottom < min_margin: self.log.warn( 'Bottom margin is too small for footer, increasing it.') opts.margin_bottom = min_margin if self.header and opts.margin_top < min_margin: self.log.warn('Top margin is too small for header, increasing it.') opts.margin_top = min_margin self.page.setViewportSize(QSize(self.doc.width(), self.doc.height())) self.render_queue = items self.total_items = len(items) mt, mb = map(self.doc.to_px, (opts.margin_top, opts.margin_bottom)) self.margin_top, self.margin_bottom = map(lambda x: int(floor(x)), (mt, mb)) self.painter = QPainter(self.doc) self.doc.set_metadata(title=pdf_metadata.title, author=pdf_metadata.author, tags=pdf_metadata.tags) self.doc_title = pdf_metadata.title self.doc_author = pdf_metadata.author self.painter.save() try: if self.cover_data is not None: p = QPixmap() p.loadFromData(self.cover_data) if not p.isNull(): self.doc.init_page() draw_image_page(QRect(*self.doc.full_page_rect), self.painter, p, preserve_aspect_ratio=self.opts. preserve_cover_aspect_ratio) self.doc.end_page() finally: self.painter.restore() QTimer.singleShot(0, self.render_book) if self.loop.exec_() == 1: raise Exception('PDF Output failed, see log for details') if self.toc is not None and len(self.toc) > 0: self.doc.add_outline(self.toc) self.painter.end() if self.doc.errors_occurred: raise Exception('PDF Output failed, see log for details') def render_book(self): if self.doc.errors_occurred: return self.loop.exit(1) try: if not self.render_queue: self.loop.exit() else: self.render_next() except: self.logger.exception('Rendering failed') self.loop.exit(1) def render_next(self): item = unicode(self.render_queue.pop(0)) self.logger.debug('Processing %s...' % item) self.current_item = item load_html(item, self.view) def render_html(self, ok): if ok: try: self.do_paged_render() except: self.log.exception('Rendering failed') self.loop.exit(1) return else: # The document is so corrupt that we can't render the page. self.logger.error('Document cannot be rendered.') self.loop.exit(1) return done = self.total_items - len(self.render_queue) self.report_progress( done / self.total_items, _('Rendered %s' % os.path.basename(self.current_item))) self.render_book() @property def current_page_num(self): return self.doc.current_page_num def load_mathjax(self): evaljs = self.view.page().mainFrame().evaluateJavaScript mjpath = P(u'viewer/mathjax').replace(os.sep, '/') if iswindows: mjpath = u'/' + mjpath if evaljs(''' window.mathjax.base = %s; mathjax.check_for_math(); mathjax.math_present ''' % (json.dumps(mjpath, ensure_ascii=False))).toBool(): self.log.debug('Math present, loading MathJax') while not evaljs('mathjax.math_loaded').toBool(): self.loop.processEvents(self.loop.ExcludeUserInputEvents) evaljs( 'document.getElementById("MathJax_Message").style.display="none";' ) def get_sections(self, anchor_map): sections = {} ci = os.path.abspath(os.path.normcase(self.current_item)) if self.toc is not None: for toc in self.toc.flat(): path = toc.abspath or None frag = toc.fragment or None if path is None: continue path = os.path.abspath(os.path.normcase(path)) if path == ci: col = 0 if frag and frag in anchor_map: col = anchor_map[frag]['column'] if col not in sections: sections[col] = toc.text or _('Untitled') return sections def do_paged_render(self): if self.paged_js is None: import uuid from calibre.utils.resources import compiled_coffeescript as cc self.paged_js = cc('ebooks.oeb.display.utils') self.paged_js += cc('ebooks.oeb.display.indexing') self.paged_js += cc('ebooks.oeb.display.paged') self.paged_js += cc('ebooks.oeb.display.mathjax') self.hf_uuid = str(uuid.uuid4()).replace('-', '') self.view.page().mainFrame().addToJavaScriptWindowObject( "py_bridge", self) self.view.page().longjs_counter = 0 evaljs = self.view.page().mainFrame().evaluateJavaScript evaljs(self.paged_js) self.load_mathjax() evaljs(''' py_bridge.__defineGetter__('value', function() { return JSON.parse(this._pass_json_value); }); py_bridge.__defineSetter__('value', function(val) { this._pass_json_value = JSON.stringify(val); }); document.body.style.backgroundColor = "white"; paged_display.set_geometry(1, %d, %d, %d); paged_display.layout(); paged_display.fit_images(); py_bridge.value = book_indexing.all_links_and_anchors(); ''' % (self.margin_top, 0, self.margin_bottom)) amap = self.bridge_value if not isinstance(amap, dict): amap = { 'links': [], 'anchors': {} } # Some javascript error occurred sections = self.get_sections(amap['anchors']) col = 0 if self.header: self.bridge_value = self.header evaljs('paged_display.header_template = py_bridge.value') if self.footer: self.bridge_value = self.footer evaljs('paged_display.footer_template = py_bridge.value') if self.header or self.footer: evaljs('paged_display.create_header_footer("%s");' % self.hf_uuid) start_page = self.current_page_num mf = self.view.page().mainFrame() while True: if col in sections: self.current_section = sections[col] self.doc.init_page() if self.header or self.footer: evaljs('paged_display.update_header_footer(%d)' % self.current_page_num) self.painter.save() mf.render(self.painter) self.painter.restore() nsl = evaljs('paged_display.next_screen_location()').toInt() self.doc.end_page() if not nsl[1] or nsl[0] <= 0: break evaljs( 'window.scrollTo(%d, 0); paged_display.position_header_footer();' % nsl[0]) if self.doc.errors_occurred: break col += 1 if not self.doc.errors_occurred: self.doc.add_links(self.current_item, start_page, amap['links'], amap['anchors'])
class CoverView(QWidget): # {{{ cover_changed = pyqtSignal(object, object) cover_removed = pyqtSignal(object) def __init__(self, vertical, parent=None): QWidget.__init__(self, parent) self._current_pixmap_size = QSize(120, 120) self.vertical = vertical self.animation = QPropertyAnimation(self, 'current_pixmap_size', self) self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo)) self.animation.setDuration(1000) self.animation.setStartValue(QSize(0, 0)) self.animation.valueChanged.connect(self.value_changed) self.setSizePolicy( QSizePolicy.Expanding if vertical else QSizePolicy.Minimum, QSizePolicy.Expanding) self.default_pixmap = QPixmap(I('book.png')) self.pixmap = self.default_pixmap self.pwidth = self.pheight = None self.data = {} self.do_layout() def value_changed(self, val): self.update() def setCurrentPixmapSize(self, val): self._current_pixmap_size = val def do_layout(self): if self.rect().width() == 0 or self.rect().height() == 0: return pixmap = self.pixmap pwidth, pheight = pixmap.width(), pixmap.height() try: self.pwidth, self.pheight = fit_image(pwidth, pheight, self.rect().width(), self.rect().height())[1:] except: self.pwidth, self.pheight = self.rect().width()-1, \ self.rect().height()-1 self.current_pixmap_size = QSize(self.pwidth, self.pheight) self.animation.setEndValue(self.current_pixmap_size) def show_data(self, data): self.animation.stop() same_item = getattr(data, 'id', True) == self.data.get('id', False) self.data = {'id': data.get('id', None)} if data.cover_data[1]: self.pixmap = QPixmap.fromImage(data.cover_data[1]) if self.pixmap.isNull() or self.pixmap.width() < 5 or \ self.pixmap.height() < 5: self.pixmap = self.default_pixmap else: self.pixmap = self.default_pixmap self.do_layout() self.update() if (not same_item and not config['disable_animations'] and self.isVisible()): self.animation.start() def paintEvent(self, event): canvas_size = self.rect() width = self.current_pixmap_size.width() extrax = canvas_size.width() - width if extrax < 0: extrax = 0 x = int(extrax / 2.) height = self.current_pixmap_size.height() extray = canvas_size.height() - height if extray < 0: extray = 0 y = int(extray / 2.) target = QRect(x, y, width, height) p = QPainter(self) p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) p.drawPixmap( target, self.pixmap.scaled(target.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) if gprefs['bd_overlay_cover_size']: sztgt = target.adjusted(0, 0, 0, -4) f = p.font() f.setBold(True) p.setFont(f) sz = u'\u00a0%d x %d\u00a0' % (self.pixmap.width(), self.pixmap.height()) flags = Qt.AlignBottom | Qt.AlignRight | Qt.TextSingleLine szrect = p.boundingRect(sztgt, flags, sz) p.fillRect(szrect.adjusted(0, 0, 0, 4), QColor(0, 0, 0, 200)) p.setPen(QPen(QColor(255, 255, 255))) p.drawText(sztgt, flags, sz) p.end() current_pixmap_size = pyqtProperty( 'QSize', fget=lambda self: self._current_pixmap_size, fset=setCurrentPixmapSize) def contextMenuEvent(self, ev): cm = QMenu(self) paste = cm.addAction(_('Paste Cover')) copy = cm.addAction(_('Copy Cover')) remove = cm.addAction(_('Remove Cover')) if not QApplication.instance().clipboard().mimeData().hasImage(): paste.setEnabled(False) copy.triggered.connect(self.copy_to_clipboard) paste.triggered.connect(self.paste_from_clipboard) remove.triggered.connect(self.remove_cover) cm.exec_(ev.globalPos()) def copy_to_clipboard(self): QApplication.instance().clipboard().setPixmap(self.pixmap) def paste_from_clipboard(self, pmap=None): if not isinstance(pmap, QPixmap): cb = QApplication.instance().clipboard() pmap = cb.pixmap() if pmap.isNull() and cb.supportsSelection(): pmap = cb.pixmap(cb.Selection) if not pmap.isNull(): self.pixmap = pmap self.do_layout() self.update() self.update_tooltip(getattr(self.parent(), 'current_path', '')) if not config['disable_animations']: self.animation.start() id_ = self.data.get('id', None) if id_ is not None: self.cover_changed.emit(id_, pixmap_to_data(pmap)) def remove_cover(self): id_ = self.data.get('id', None) self.pixmap = self.default_pixmap self.do_layout() self.update() if id_ is not None: self.cover_removed.emit(id_) def update_tooltip(self, current_path): try: sz = self.pixmap.size() except: sz = QSize(0, 0) self.setToolTip('<p>' + _('Double-click to open Book Details window') + '<br><br>' + _('Path') + ': ' + current_path + '<br><br>' + _('Cover size: %(width)d x %(height)d') % dict(width=sz.width(), height=sz.height()))
class CoverView(QWidget): u""" TODO: 实现点击图书显示封面的效果(再加一个coverflow的效果就完美了), 看来现在暂时搞不定了 """ cover_changed = pyqtSignal(object, object) cover_removed = pyqtSignal(object) open_cover_width = pyqtSignal(object, object) def __init__(self, vertical, parent=None): QWidget.__init__(self, parent) self._current_pixmap_size = QSize(120, 120) self.vertical = vertical self.animation = QPropertyAnimation(self, b'current_pixmap_size', self) self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo)) self.animation.setDuration(1000) self.animation.setStartValue(QSize(0, 0)) # self.animation.valueChanged.connect(self.value_changed) self.setSizePolicy( QSizePolicy.Expanding if vertical else QSizePolicy.Minimum, QSizePolicy.Expanding) # self.default_pixmap = QPixmap(os.getcwd() + "/src/gui/code Examples/book.icns") self.default_pixmap = QPixmap(":/back.png") self.pixmap = self.default_pixmap self.pwidth = self.pheight = None self.data = {} self.do_layout() def value_changed(self, val): self.update() def setCurrentPixmapSize(self, val): self._current_pixmap_size = val def do_layout(self): if self.rect().width() == 0 or self.rect().height() == 0: return pixmap = self.pixmap pwidth, pheight = pixmap.width(), pixmap.height() try: self.pwidth, self.pheight = fit_image(pwidth, pheight, self.rect().width, self.rect().height())[1:] except: self.pwidth, self.pheight = self.rect().width() - 1, self.rect( ).height() - 1 self.current_pixmap_size = QSize(self.pwidth, self.pheight) self.animation.setEndValue(self.current_pixmap_size) def show_data(self, current_book): # self.animation.stop() # TODO if False: pass else: self.pixmap = self.default_pixmap # self.do_layout() # self.updatte() # self.animation.start() current_pixmap_size = pyqtProperty( 'QSize', fget=lambda self: self._current_pixmap_size, fset=setCurrentPixmapSize) def value_changed(self, val): # self.update() pass
class Document(QWebPage): # {{{ page_turn = pyqtSignal(object) mark_element = pyqtSignal(QWebElement) settings_changed = pyqtSignal() def set_font_settings(self, opts): settings = self.settings() apply_settings(settings, opts) def do_config(self, parent=None): d = ConfigDialog(self.shortcuts, parent) if d.exec_() == QDialog.Accepted: opts = config().parse() self.apply_settings(opts) def apply_settings(self, opts): with self.page_position: self.set_font_settings(opts) self.set_user_stylesheet(opts) self.misc_config(opts) self.settings_changed.emit() self.after_load() def __init__(self, shortcuts, parent=None, debug_javascript=False): QWebPage.__init__(self, parent) self.setObjectName("py_bridge") self.in_paged_mode = False # Use this to pass arbitrary JSON encodable objects between python and # javascript. In python get/set the value as: self.bridge_value. In # javascript, get/set the value as: py_bridge.value self.bridge_value = None self.first_load = True self.debug_javascript = debug_javascript self.anchor_positions = {} self.index_anchors = set() self.current_language = None self.loaded_javascript = False self.js_loader = JavaScriptLoader( dynamic_coffeescript=self.debug_javascript) self.in_fullscreen_mode = False self.setLinkDelegationPolicy(self.DelegateAllLinks) self.scroll_marks = [] self.shortcuts = shortcuts pal = self.palette() pal.setBrush(QPalette.Background, QColor(0xee, 0xee, 0xee)) self.setPalette(pal) self.page_position = PagePosition(self) settings = self.settings() # Fonts self.all_viewer_plugins = tuple(all_viewer_plugins()) for pl in self.all_viewer_plugins: pl.load_fonts() opts = config().parse() self.set_font_settings(opts) # Security settings.setAttribute(QWebSettings.JavaEnabled, False) settings.setAttribute(QWebSettings.PluginsEnabled, False) settings.setAttribute(QWebSettings.JavascriptCanOpenWindows, False) settings.setAttribute(QWebSettings.JavascriptCanAccessClipboard, False) # Miscellaneous settings.setAttribute(QWebSettings.LinksIncludedInFocusChain, True) settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True) self.set_user_stylesheet(opts) self.misc_config(opts) # Load javascript self.mainFrame().javaScriptWindowObjectCleared.connect( self.add_window_objects) self.turn_off_internal_scrollbars() def turn_off_internal_scrollbars(self): mf = self.mainFrame() mf.setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff) mf.setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) def set_user_stylesheet(self, opts): bg = opts.background_color or 'white' brules = ['background-color: %s !important'%bg] prefix = ''' body { %s } '''%('; '.join(brules)) if opts.text_color: prefix += '\n\nbody, p, div { color: %s !important }'%opts.text_color raw = prefix + opts.user_css raw = '::selection {background:#ffff00; color:#000;}\n'+raw data = 'data:text/css;charset=utf-8;base64,' data += b64encode(raw.encode('utf-8')) self.settings().setUserStyleSheetUrl(QUrl(data)) def findText(self, q, flags): if self.hyphenatable: q = unicode(q) hyphenated_q = self.javascript( 'hyphenate_text(%s, "%s")' % (json.dumps(q, ensure_ascii=False), self.loaded_lang), typ='string') if QWebPage.findText(self, hyphenated_q, flags): return True return QWebPage.findText(self, q, flags) def misc_config(self, opts): self.hyphenate = opts.hyphenate self.hyphenate_default_lang = opts.hyphenate_default_lang self.do_fit_images = opts.fit_images self.page_flip_duration = opts.page_flip_duration self.enable_page_flip = self.page_flip_duration > 0.1 self.font_magnification_step = opts.font_magnification_step self.wheel_flips_pages = opts.wheel_flips_pages self.line_scrolling_stops_on_pagebreaks = opts.line_scrolling_stops_on_pagebreaks screen_width = QApplication.desktop().screenGeometry().width() # Leave some space for the scrollbar and some border self.max_fs_width = min(opts.max_fs_width, screen_width-50) self.fullscreen_clock = opts.fullscreen_clock self.fullscreen_scrollbar = opts.fullscreen_scrollbar self.fullscreen_pos = opts.fullscreen_pos self.start_in_fullscreen = opts.start_in_fullscreen self.show_fullscreen_help = opts.show_fullscreen_help self.use_book_margins = opts.use_book_margins self.cols_per_screen = opts.cols_per_screen self.side_margin = opts.side_margin self.top_margin, self.bottom_margin = opts.top_margin, opts.bottom_margin self.show_controls = opts.show_controls def fit_images(self): if self.do_fit_images and not self.in_paged_mode: self.javascript('setup_image_scaling_handlers()') def add_window_objects(self): self.mainFrame().addToJavaScriptWindowObject("py_bridge", self) self.javascript(''' py_bridge.__defineGetter__('value', function() { return JSON.parse(this._pass_json_value); }); py_bridge.__defineSetter__('value', function(val) { this._pass_json_value = JSON.stringify(val); }); ''') self.loaded_javascript = False def load_javascript_libraries(self): if self.loaded_javascript: return self.loaded_javascript = True evaljs = self.mainFrame().evaluateJavaScript self.loaded_lang = self.js_loader(evaljs, self.current_language, self.hyphenate_default_lang) mjpath = P(u'viewer/mathjax').replace(os.sep, '/') if iswindows: mjpath = u'/' + mjpath self.javascript(u'window.mathjax.base = %s'%(json.dumps(mjpath, ensure_ascii=False))) for pl in self.all_viewer_plugins: pl.load_javascript(evaljs) evaljs('py_bridge.mark_element.connect(window.calibre_extract.mark)') @pyqtSignature("") def animated_scroll_done(self): self.emit(SIGNAL('animated_scroll_done()')) @property def hyphenatable(self): # Qt fails to render soft hyphens correctly on windows xp return not isxp and self.hyphenate and getattr(self, 'loaded_lang', '') @pyqtSignature("") def init_hyphenate(self): if self.hyphenatable: self.javascript('do_hyphenation("%s")'%self.loaded_lang) @pyqtSlot(int) def page_turn_requested(self, backwards): self.page_turn.emit(bool(backwards)) def _pass_json_value_getter(self): val = json.dumps(self.bridge_value) return QString(val) def _pass_json_value_setter(self, value): self.bridge_value = json.loads(unicode(value)) _pass_json_value = pyqtProperty(QString, fget=_pass_json_value_getter, fset=_pass_json_value_setter) def after_load(self, last_loaded_path=None): self.javascript('window.paged_display.read_document_margins()') self.set_bottom_padding(0) self.fit_images() self.init_hyphenate() self.javascript('full_screen.save_margins()') if self.in_fullscreen_mode: self.switch_to_fullscreen_mode() if self.in_paged_mode: self.switch_to_paged_mode(last_loaded_path=last_loaded_path) self.read_anchor_positions(use_cache=False) evaljs = self.mainFrame().evaluateJavaScript for pl in self.all_viewer_plugins: pl.run_javascript(evaljs) self.javascript('window.mathjax.check_for_math()') self.first_load = False def colors(self): self.javascript(''' bs = getComputedStyle(document.body); py_bridge.value = [bs.backgroundColor, bs.color] ''') ans = self.bridge_value return (ans if isinstance(ans, list) else ['white', 'black']) def read_anchor_positions(self, use_cache=True): self.bridge_value = tuple(self.index_anchors) self.javascript(u''' py_bridge.value = book_indexing.anchor_positions(py_bridge.value, %s); '''%('true' if use_cache else 'false')) self.anchor_positions = self.bridge_value if not isinstance(self.anchor_positions, dict): # Some weird javascript error happened self.anchor_positions = {} return {k:tuple(v) for k, v in self.anchor_positions.iteritems()} def switch_to_paged_mode(self, onresize=False, last_loaded_path=None): if onresize and not self.loaded_javascript: return self.javascript(''' window.paged_display.use_document_margins = %s; window.paged_display.set_geometry(%d, %d, %d, %d); '''%( ('true' if self.use_book_margins else 'false'), self.cols_per_screen, self.top_margin, self.side_margin, self.bottom_margin )) force_fullscreen_layout = bool(getattr(last_loaded_path, 'is_single_page', False)) f = 'true' if force_fullscreen_layout else 'false' side_margin = self.javascript('window.paged_display.layout(%s)'%f, typ=int) # Setup the contents size to ensure that there is a right most margin. # Without this WebKit renders the final column with no margin, as the # columns extend beyond the boundaries (and margin) of body mf = self.mainFrame() sz = mf.contentsSize() scroll_width = self.javascript('document.body.scrollWidth', int) # At this point sz.width() is not reliable, presumably because Qt # has not yet been updated if scroll_width > self.window_width: sz.setWidth(scroll_width+side_margin) self.setPreferredContentsSize(sz) self.javascript('window.paged_display.fit_images()') @property def column_boundaries(self): if not self.loaded_javascript: return (0, 1) self.javascript(u'py_bridge.value = paged_display.column_boundaries()') return tuple(self.bridge_value) def after_resize(self): if self.in_paged_mode: self.setPreferredContentsSize(QSize()) self.switch_to_paged_mode(onresize=True) self.javascript('window.mathjax.after_resize()') def switch_to_fullscreen_mode(self): self.in_fullscreen_mode = True self.javascript('full_screen.on(%d, %s)'%(self.max_fs_width, 'true' if self.in_paged_mode else 'false')) def switch_to_window_mode(self): self.in_fullscreen_mode = False self.javascript('full_screen.off(%s)'%('true' if self.in_paged_mode else 'false')) @pyqtSignature("QString") def debug(self, msg): prints(msg) def reference_mode(self, enable): self.javascript(('enter' if enable else 'leave')+'_reference_mode()') def set_reference_prefix(self, prefix): self.javascript('reference_prefix = "%s"'%prefix) def goto(self, ref): self.javascript('goto_reference("%s")'%ref) def goto_bookmark(self, bm): if bm['type'] == 'legacy': bm = bm['pos'] bm = bm.strip() if bm.startswith('>'): bm = bm[1:].strip() self.javascript('scroll_to_bookmark("%s")'%bm) elif bm['type'] == 'cfi': self.page_position.to_pos(bm['pos']) def javascript(self, string, typ=None): ans = self.mainFrame().evaluateJavaScript(string) if typ in {'int', int}: ans = ans.toInt() if ans[1]: return ans[0] return 0 if typ in {'float', float}: ans = ans.toReal() return ans[0] if ans[1] else 0.0 if typ == 'string': return unicode(ans.toString()) return ans def javaScriptConsoleMessage(self, msg, lineno, msgid): if self.debug_javascript: prints(msg) else: return QWebPage.javaScriptConsoleMessage(self, msg, lineno, msgid) def javaScriptAlert(self, frame, msg): if self.debug_javascript: prints(msg) else: return QWebPage.javaScriptAlert(self, frame, msg) def scroll_by(self, dx=0, dy=0): self.mainFrame().scroll(dx, dy) def scroll_to(self, x=0, y=0): self.mainFrame().setScrollPosition(QPoint(x, y)) def jump_to_anchor(self, anchor): if not self.loaded_javascript: return self.javascript('window.paged_display.jump_to_anchor("%s")'%anchor) def element_ypos(self, elem): ans, ok = elem.evaluateJavaScript('$(this).offset().top').toInt() if not ok: raise ValueError('No ypos found') return ans def elem_outer_xml(self, elem): return unicode(elem.toOuterXml()) def bookmark(self): pos = self.page_position.current_pos return {'type':'cfi', 'pos':pos} @property def at_bottom(self): return self.height - self.ypos <= self.window_height @property def at_top(self): return self.ypos <=0 def test(self): pass @property def ypos(self): return self.mainFrame().scrollPosition().y() @property def window_height(self): return self.javascript('window.innerHeight', 'int') @property def window_width(self): return self.javascript('window.innerWidth', 'int') @property def xpos(self): return self.mainFrame().scrollPosition().x() @dynamic_property def scroll_fraction(self): def fget(self): if self.in_paged_mode: return self.javascript(''' ans = 0.0; if (window.paged_display) { ans = window.paged_display.current_pos(); } ans;''', typ='float') else: try: return abs(float(self.ypos)/(self.height-self.window_height)) except ZeroDivisionError: return 0. def fset(self, val): if self.in_paged_mode and self.loaded_javascript: self.javascript('paged_display.scroll_to_pos(%f)'%val) else: npos = val * (self.height - self.window_height) if npos < 0: npos = 0 self.scroll_to(x=self.xpos, y=npos) return property(fget=fget, fset=fset) @property def hscroll_fraction(self): try: return float(self.xpos)/self.width except ZeroDivisionError: return 0. @property def height(self): # Note that document.body.offsetHeight does not include top and bottom # margins on body and in some cases does not include the top margin on # the first element inside body either. See ticket #8791 for an example # of the latter. q = self.mainFrame().contentsSize().height() if q < 0: # Don't know if this is still needed, but it can't hurt j = self.javascript('document.body.offsetHeight', 'int') if j >= 0: q = j return q @property def width(self): return self.mainFrame().contentsSize().width() # offsetWidth gives inaccurate results def set_bottom_padding(self, amount): s = QSize(-1, -1) if amount == 0 else QSize(self.viewportSize().width(), self.height+amount) self.setPreferredContentsSize(s) def extract_node(self): return unicode(self.mainFrame().evaluateJavaScript( 'window.calibre_extract.extract()').toString())
class SlideFlip(QWidget): # API {{{ # In addition the isVisible() and setVisible() methods must be present def __init__(self, parent): QWidget.__init__(self, parent) self.setGeometry(0, 0, 1, 1) self._current_width = 0 self.before_image = self.after_image = None self.animation = QPropertyAnimation(self, 'current_width', self) self.setVisible(False) self.animation.valueChanged.connect(self.update) self.animation.finished.connect(self.finished) self.flip_forwards = True self.setAttribute(Qt.WA_OpaquePaintEvent) @property def running(self): 'True iff animation is currently running' return self.animation.state() == self.animation.Running def initialize(self, image, forwards=True): ''' Initialize the flipper, causes the flipper to show itself displaying the full `image`. :param image: The image to display as background :param forwards: If True flipper will flip forwards, otherwise backwards ''' self.flip_forwards = forwards self.before_image = QPixmap.fromImage(image) self.after_image = None self.setGeometry(0, 0, image.width(), image.height()) self.setVisible(True) def __call__(self, image, duration=0.5): ''' Start the animation. You must have called :meth:`initialize` first. :param duration: Animation duration in seconds. ''' if self.running: return self.after_image = QPixmap.fromImage(image) if self.flip_forwards: self.animation.setStartValue(image.width()) self.animation.setEndValue(0) t = self.before_image self.before_image = self.after_image self.after_image = t self.animation.setEasingCurve(QEasingCurve(QEasingCurve.InExpo)) else: self.animation.setStartValue(0) self.animation.setEndValue(image.width()) self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo)) self.animation.setDuration(duration * 1000) self.animation.start() # }}} def finished(self): self.setVisible(False) self.before_image = self.after_image = None def paintEvent(self, ev): if self.before_image is None: return canvas_size = self.rect() p = QPainter(self) p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) p.drawPixmap(canvas_size, self.before_image, self.before_image.rect()) if self.after_image is not None: width = self._current_width iw = self.after_image.width() sh = min(self.after_image.height(), canvas_size.height()) if self.flip_forwards: source = QRect(max(0, iw - width), 0, width, sh) else: source = QRect(0, 0, width, sh) target = QRect(source) target.moveLeft(0) p.drawPixmap(target, self.after_image, source) p.end() def set_current_width(self, val): self._current_width = val current_width = pyqtProperty('int', fget=lambda self: self._current_width, fset=set_current_width)
class PDFWriter(QObject): # {{{ def __init__(self, opts, log, cover_data=None, toc=None): from calibre.gui2 import is_ok_to_use_qt from calibre.utils.podofo import get_podofo if not is_ok_to_use_qt(): raise Exception('Not OK to use Qt') QObject.__init__(self) self.logger = self.log = log self.podofo = get_podofo() self.doc = self.podofo.PDFDoc() self.loop = QEventLoop() self.view = QWebView() self.page = Page(opts, self.log) self.view.setPage(self.page) self.view.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform) self.view.loadFinished.connect(self._render_html, type=Qt.QueuedConnection) for x in (Qt.Horizontal, Qt.Vertical): self.view.page().mainFrame().setScrollBarPolicy( x, Qt.ScrollBarAlwaysOff) self.render_queue = [] self.combine_queue = [] self.tmp_path = PersistentTemporaryDirectory(u'_pdf_output_parts') self.opts = opts self.cover_data = cover_data self.paged_js = None self.toc = toc def dump(self, items, out_stream, pdf_metadata): self.metadata = pdf_metadata self._delete_tmpdir() self.outline = Outline(self.toc, items) self.render_queue = items self.combine_queue = [] self.out_stream = out_stream self.insert_cover() self.render_succeeded = False self.current_page_num = self.doc.page_count() self.combine_queue.append( os.path.join(self.tmp_path, 'qprinter_out.pdf')) self.first_page = True self.setup_printer(self.combine_queue[-1]) QTimer.singleShot(0, self._render_book) self.loop.exec_() if self.painter is not None: self.painter.end() if self.printer is not None: self.printer.abort() if not self.render_succeeded: raise Exception('Rendering HTML to PDF failed') def _render_book(self): try: if len(self.render_queue) == 0: self._write() else: self._render_next() except: self.logger.exception('Rendering failed') self.loop.exit(1) def _render_next(self): item = unicode(self.render_queue.pop(0)) self.logger.debug('Processing %s...' % item) self.current_item = item load_html(item, self.view) def _render_html(self, ok): if ok: self.do_paged_render() else: # The document is so corrupt that we can't render the page. self.logger.error('Document cannot be rendered.') self.loop.exit(0) return self._render_book() def _pass_json_value_getter(self): val = json.dumps(self.bridge_value) return QString(val) def _pass_json_value_setter(self, value): self.bridge_value = json.loads(unicode(value)) _pass_json_value = pyqtProperty(QString, fget=_pass_json_value_getter, fset=_pass_json_value_setter) def setup_printer(self, outpath): self.printer = self.painter = None printer = get_pdf_printer(self.opts, output_file_name=outpath) painter = QPainter(printer) zoomx = printer.logicalDpiX() / self.view.logicalDpiX() zoomy = printer.logicalDpiY() / self.view.logicalDpiY() painter.scale(zoomx, zoomy) pr = printer.pageRect() self.printer, self.painter = printer, painter self.viewport_size = QSize(pr.width() / zoomx, pr.height() / zoomy) self.page.setViewportSize(self.viewport_size) def do_paged_render(self): if self.paged_js is None: from calibre.utils.resources import compiled_coffeescript self.paged_js = compiled_coffeescript('ebooks.oeb.display.utils') self.paged_js += compiled_coffeescript( 'ebooks.oeb.display.indexing') self.paged_js += compiled_coffeescript('ebooks.oeb.display.paged') self.view.page().mainFrame().addToJavaScriptWindowObject( "py_bridge", self) evaljs = self.view.page().mainFrame().evaluateJavaScript evaljs(self.paged_js) evaljs(''' py_bridge.__defineGetter__('value', function() { return JSON.parse(this._pass_json_value); }); py_bridge.__defineSetter__('value', function(val) { this._pass_json_value = JSON.stringify(val); }); document.body.style.backgroundColor = "white"; paged_display.set_geometry(1, 0, 0, 0); paged_display.layout(); paged_display.fit_images(); ''') mf = self.view.page().mainFrame() start_page = self.current_page_num if not self.first_page: start_page += 1 while True: if not self.first_page: if self.printer.newPage(): self.current_page_num += 1 self.first_page = False mf.render(self.painter) nsl = evaljs('paged_display.next_screen_location()').toInt() if not nsl[1] or nsl[0] <= 0: break evaljs('window.scrollTo(%d, 0)' % nsl[0]) self.bridge_value = tuple(self.outline.anchor_map[self.current_item]) evaljs( 'py_bridge.value = book_indexing.anchor_positions(py_bridge.value)' ) amap = self.bridge_value if not isinstance(amap, dict): amap = {} # Some javascript error occurred self.outline.set_pos(self.current_item, None, start_page, 0) for anchor, x in amap.iteritems(): pagenum, ypos = x self.outline.set_pos(self.current_item, anchor, start_page + pagenum, ypos) def append_doc(self, outpath): doc = self.podofo.PDFDoc() with open(outpath, 'rb') as f: raw = f.read() doc.load(raw) self.doc.append(doc) def _delete_tmpdir(self): if os.path.exists(self.tmp_path): shutil.rmtree(self.tmp_path, True) self.tmp_path = PersistentTemporaryDirectory('_pdf_output_parts') def insert_cover(self): if not isinstance(self.cover_data, bytes): return item_path = os.path.join(self.tmp_path, 'cover.pdf') printer = get_pdf_printer(self.opts, output_file_name=item_path, for_comic=True) self.combine_queue.insert(0, item_path) p = QPixmap() p.loadFromData(self.cover_data) if not p.isNull(): painter = QPainter(printer) draw_image_page( printer, painter, p, preserve_aspect_ratio=self.opts.preserve_cover_aspect_ratio) painter.end() self.append_doc(item_path) printer.abort() def _write(self): self.painter.end() self.printer.abort() self.painter = self.printer = None self.append_doc(self.combine_queue[-1]) try: self.doc.creator = u'%s %s [http://calibre-ebook.com]' % ( __appname__, __version__) self.doc.title = self.metadata.title self.doc.author = self.metadata.author if self.metadata.tags: self.doc.keywords = self.metadata.tags self.outline(self.doc) self.doc.save_to_fileobj(self.out_stream) self.render_succeeded = True finally: self._delete_tmpdir() self.loop.exit(0)