Пример #1
0
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);
        });
        ''')
Пример #2
0
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'])
Пример #3
0
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()))
Пример #4
0
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
Пример #5
0
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())
Пример #6
0
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)
Пример #7
0
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)