Esempio n. 1
0
 def _wait_for_replies(self, reply_count, timeout):
     final_time = time.time() + (self.default_timeout if timeout is default_timeout else timeout)
     loop = QEventLoop(self)
     while (time.time() < final_time and self.nam.reply_count <
             reply_count):
         loop.processEvents()
         time.sleep(0.1)
     if self.nam.reply_count < reply_count:
         raise Timeout('Waiting for replies took longer than %d seconds' %
                 timeout)
Esempio n. 2
0
    def do_print(self, printer):
        painter = QPainter(printer)
        zoomx = printer.logicalDpiX() / self.view.logicalDpiX()
        zoomy = printer.logicalDpiY() / self.view.logicalDpiY()
        painter.scale(zoomx, zoomy)
        pr = printer.pageRect()
        self.view.page().setViewportSize(
            QSize(pr.width() / zoomx,
                  pr.height() / zoomy))
        evaljs = self.mf.evaluateJavaScript
        loop = QEventLoop(self)
        pagenum = 0
        from_, to = printer.fromPage(), printer.toPage()
        first = True

        for path in self.iterator.spine:
            self.loaded_ok = None
            load_html(path,
                      self.view,
                      codec=getattr(path, 'encoding', 'utf-8'),
                      mime_type=getattr(path, 'mime_type', None))
            while self.loaded_ok is None:
                loop.processEvents(loop.ExcludeUserInputEvents)
            if not self.loaded_ok:
                return error_dialog(self.parent(),
                                    _('Failed to render'),
                                    _('Failed to render document %s') % path,
                                    show=True)
            evaljs(self.paged_js)
            evaljs('''
                document.body.style.backgroundColor = "white";
                paged_display.set_geometry(1, 0, 0, 0);
                paged_display.layout();
                paged_display.fit_images();
            ''')

            while True:
                pagenum += 1
                if (pagenum >= from_ and (to == 0 or pagenum <= to)):
                    if not first:
                        printer.newPage()
                    first = False
                    self.mf.render(painter)
                try:
                    nsl = int(evaljs('paged_display.next_screen_location()'))
                except (TypeError, ValueError):
                    break
                if nsl <= 0:
                    break
                evaljs('window.scrollTo(%d, 0)' % nsl)

        painter.end()
Esempio n. 3
0
    def do_print(self, printer):
        painter = QPainter(printer)
        zoomx = printer.logicalDpiX()/self.view.logicalDpiX()
        zoomy = printer.logicalDpiY()/self.view.logicalDpiY()
        painter.scale(zoomx, zoomy)
        pr = printer.pageRect()
        self.view.page().setViewportSize(QSize(pr.width()/zoomx,
            pr.height()/zoomy))
        evaljs = self.mf.evaluateJavaScript
        loop = QEventLoop(self)
        pagenum = 0
        from_, to = printer.fromPage(), printer.toPage()
        first = True

        for path in self.iterator.spine:
            self.loaded_ok = None
            load_html(path, self.view, codec=getattr(path, 'encoding', 'utf-8'),
                    mime_type=getattr(path, 'mime_type', None))
            while self.loaded_ok is None:
                loop.processEvents(loop.ExcludeUserInputEvents)
            if not self.loaded_ok:
                return error_dialog(self.parent(), _('Failed to render'),
                        _('Failed to render document %s')%path, show=True)
            evaljs(self.paged_js)
            evaljs('''
                document.body.style.backgroundColor = "white";
                paged_display.set_geometry(1, 0, 0, 0);
                paged_display.layout();
                paged_display.fit_images();
            ''')

            while True:
                pagenum += 1
                if (pagenum >= from_ and (to == 0 or pagenum <= to)):
                    if not first:
                        printer.newPage()
                    first = False
                    self.mf.render(painter)
                try:
                    nsl = int(evaljs('paged_display.next_screen_location()'))
                except (TypeError, ValueError):
                    break
                if nsl <= 0:
                    break
                evaljs('window.scrollTo(%d, 0)'%nsl)

        painter.end()
Esempio n. 4
0
    def download_file(self, url_or_selector_or_qwe, timeout=60):
        '''
        Download unsupported content: i.e. files the browser cannot handle
        itself or files marked for saving as files by the website. Useful if
        you want to download something like an epub file after authentication.

        You can pass in either the url to the file to be downloaded, or a
        selector that points to an element to be clicked on the current page
        which will cause the file to be downloaded.
        '''
        ans = [False, None, []]
        loop = QEventLoop(self)
        start_time = time.time()
        end_time = start_time + timeout
        self.page.unsupportedContent.disconnect(self.page.on_unsupported_content)
        try:
            def download(reply):
                if ans[0]:
                    reply.abort()  # We only handle the first unsupported download
                    return
                ans[0] = True
                while not reply.isFinished() and end_time > time.time():
                    if not loop.processEvents():
                        time.sleep(0.01)
                    raw = bytes(bytearray(reply.readAll()))
                    if raw:
                        ans[-1].append(raw)
                if not reply.isFinished():
                    ans[1] = Timeout('Loading of %r took longer than %d seconds'%(url_or_selector_or_qwe, timeout))
                ans[-1].append(bytes(bytearray(reply.readAll())))
            self.page.unsupportedContent.connect(download)
            if hasattr(url_or_selector_or_qwe, 'rstrip') and re.match('[a-z]+://', url_or_selector_or_qwe) is not None:
                # We have a URL
                self.page.mainFrame().load(QUrl(url_or_selector_or_qwe))
            else:
                self.click(url_or_selector_or_qwe, wait_for_load=False)
            lw = LoadWatcher(self.page)
            while not ans[0] and lw.is_loading and end_time > time.time():
                if not loop.processEvents():
                    time.sleep(0.01)
            if not ans[0]:
                raise NotAFile('%r does not point to a downloadable file. You can only'
                                 ' use this method to download files that the browser cannot handle'
                                 ' natively. Or files that are marked with the '
                                 ' content-disposition: attachment header' % url_or_selector_or_qwe)
            if ans[1] is not None:
                raise ans[1]
            return b''.join(ans[-1])
        finally:
            self.page.unsupportedContent.disconnect()
            self.page.unsupportedContent.connect(self.page.on_unsupported_content)
Esempio n. 5
0
    def _wait_for_load(self, timeout, url=None):
        timeout = self.default_timeout if timeout is default_timeout else timeout
        loop = QEventLoop(self)
        start_time = time.time()
        end_time = start_time + timeout
        lw = LoadWatcher(self.page, parent=self)
        while lw.is_loading and end_time > time.time():
            if not loop.processEvents():
                time.sleep(0.01)
        if lw.is_loading:
            raise Timeout('Loading of %r took longer than %d seconds'%(
                url, timeout))

        return lw.loaded_ok
Esempio n. 6
0
class PDFWriter(QObject):
    @pyqtSlot(result=unicode_type)
    def title(self):
        return self.doc_title

    @pyqtSlot(result=unicode_type)
    def author(self):
        return self.doc_author

    @pyqtSlot(result=unicode_type)
    def section(self):
        return self.current_section

    @pyqtSlot(result=unicode_type)
    def tl_section(self):
        return self.current_tl_section

    def __init__(self, opts, log, cover_data=None, toc=None):
        from calibre.gui2 import must_use_qt
        must_use_qt()
        QObject.__init__(self)

        self.logger = self.log = log
        self.mathjax_dir = P('mathjax', allow_user_override=False)
        current_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)
        self.view.loadProgress.connect(self.load_progress)
        self.ignore_failure = None
        self.hang_check_timer = t = QTimer(self)
        t.timeout.connect(self.hang_check)
        t.setInterval(1000)

        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 = ''
        self.current_tl_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()

        def margin(which):
            val = getattr(opts, 'pdf_page_margin_' + which)
            if val == 0.0:
                val = getattr(opts, 'margin_' + which)
            return val

        ml, mr, mt, mb = map(margin, 'left right top bottom'.split())
        # 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)
        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,
                             opts=opts,
                             mark_links=opts.pdf_mark_links,
                             page_margins=(ml, mr, mt, mb))
        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 = 1.5 * opts._final_base_font_size
        if self.footer and mb < min_margin:
            self.log.warn(
                'Bottom margin is too small for footer, increasing it to %.1fpts'
                % min_margin)
            mb = min_margin
        if self.header and mt < min_margin:
            self.log.warn(
                'Top margin is too small for header, increasing it to %.1fpts'
                % min_margin)
            mt = 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, (mt, mb))
        self.margin_top, self.margin_bottom = map(lambda x: int(floor(x)),
                                                  (mt, mb))

        self.painter = QPainter(self.doc)
        try:
            self.book_language = pdf_metadata.mi.languages[0]
        except Exception:
            self.book_language = 'eng'
        self.doc.set_metadata(title=pdf_metadata.title,
                              author=pdf_metadata.author,
                              tags=pdf_metadata.tags,
                              mi=pdf_metadata.mi)
        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()
                try:
                    p.loadFromData(self.cover_data)
                except TypeError:
                    self.log.warn(
                        'This ebook does not have a raster cover, cannot generate cover for PDF'
                        '. Cover type: %s' % type(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_inline_toc(self):
        evaljs = self.view.page().mainFrame().evaluateJavaScript
        self.rendered_inline_toc = True
        from calibre.ebooks.pdf.render.toc import toc_as_html
        raw = toc_as_html(self.toc, self.doc, self.opts, evaljs)
        pt = PersistentTemporaryFile('_pdf_itoc.htm')
        pt.write(raw)
        pt.close()
        self.render_queue.append(pt.name)
        self.render_next()

    def render_book(self):
        if self.doc.errors_occurred:
            return self.loop.exit(1)
        try:
            if not self.render_queue:
                if self.opts.pdf_add_toc and self.toc is not None and len(
                        self.toc) > 0 and not hasattr(self,
                                                      'rendered_inline_toc'):
                    return self.render_inline_toc()
                self.loop.exit()
            else:
                self.render_next()
        except:
            self.logger.exception('Rendering failed')
            self.loop.exit(1)

    def render_next(self):
        item = unicode_type(self.render_queue.pop(0))

        self.logger.debug('Processing %s...' % item)
        self.current_item = item
        load_html(item, self.view)
        self.last_load_progress_at = monotonic()
        self.hang_check_timer.start()

    def load_progress(self, progress):
        self.last_load_progress_at = monotonic()

    def hang_check(self):
        if monotonic() - self.last_load_progress_at > 60:
            self.log.warn('Timed out waiting for %s to render' %
                          self.current_item)
            self.ignore_failure = self.current_item
            self.view.stop()

    def render_html(self, ok):
        self.hang_check_timer.stop()
        if self.ignore_failure == self.current_item:
            ok = True
        self.ignore_failure = None
        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 %s cannot be rendered.' %
                              self.current_item)
            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 = self.mathjax_dir.replace(os.sep, '/')
        if iswindows:
            mjpath = u'/' + mjpath
        if bool(
                evaljs('''
                    window.mathjax.base = %s;
                    mathjax.check_for_math(); mathjax.math_present
                    ''' % (json.dumps(mjpath, ensure_ascii=False)))):
            self.log.debug('Math present, loading MathJax')
            while not bool(evaljs('mathjax.math_loaded')):
                self.loop.processEvents(self.loop.ExcludeUserInputEvents)
            # give the MathJax fonts time to load
            for i in range(5):
                self.loop.processEvents(self.loop.ExcludeUserInputEvents)
            evaljs(
                'document.getElementById("MathJax_Message").style.display="none";'
            )

    def load_header_footer_images(self):
        from calibre.utils.monotonic import monotonic
        evaljs = self.view.page().mainFrame().evaluateJavaScript
        st = monotonic()
        while not evaljs('paged_display.header_footer_images_loaded()'):
            self.loop.processEvents(self.loop.ExcludeUserInputEvents)
            if monotonic() - st > 5:
                self.log.warn(
                    'Header and footer images have not loaded in 5 seconds, ignoring'
                )
                break

    def get_sections(self, anchor_map, only_top_level=False):
        sections = defaultdict(list)
        ci = os.path.abspath(os.path.normcase(self.current_item))
        if self.toc is not None:
            tocentries = self.toc.top_level_items(
            ) if only_top_level else self.toc.flat()
            for toc in tocentries:
                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']
                    sections[col].append(toc.text or _('Untitled'))

        return sections

    def hyphenate(self, evaljs):
        evaljs(u'''\
        Hyphenator.config(
            {
            'minwordlength'    : 6,
            // 'hyphenchar'     : '|',
            'displaytogglebox' : false,
            'remoteloading'    : false,
            'doframes'         : true,
            'defaultlanguage'  : 'en',
            'storagetype'      : 'session',
            'onerrorhandler'   : function (e) {
                                    console.log(e);
                                }
            });
        Hyphenator.hyphenate(document.body, "%s");
        ''' % self.hyphenate_lang)

    def convert_page_margins(self, doc_margins):
        ans = [0, 0, 0, 0]

        def convert(name, idx, vertical=True):
            m = doc_margins.get(name)
            if m is None:
                ans[idx] = getattr(self.doc.engine, '{}_margin'.format(name))
            else:
                ans[idx] = m

        convert('left', 0,
                False), convert('top',
                                1), convert('right', 2,
                                            False), convert('bottom', 3)
        return ans

    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').decode('utf-8')
            self.paged_js += cc('ebooks.oeb.display.indexing').decode('utf-8')
            self.paged_js += cc('ebooks.oeb.display.paged').decode('utf-8')
            self.paged_js += cc('ebooks.oeb.display.mathjax').decode('utf-8')
            if self.opts.pdf_hyphenate:
                self.paged_js += P('viewer/hyphenate/Hyphenator.js',
                                   data=True).decode('utf-8')
                hjs, self.hyphenate_lang = load_hyphenator_dicts(
                    {}, self.book_language)
                self.paged_js += hjs
            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()
        if self.opts.pdf_hyphenate:
            self.hyphenate(evaljs)

        margin_top, margin_bottom = self.margin_top, self.margin_bottom
        page_margins = None
        if self.opts.pdf_use_document_margins:
            doc_margins = evaljs(
                'document.documentElement.getAttribute("data-calibre-pdf-output-page-margins")'
            )
            try:
                doc_margins = json.loads(doc_margins)
            except Exception:
                doc_margins = None
            if doc_margins and isinstance(doc_margins, dict):
                doc_margins = {
                    k: float(v)
                    for k, v in iteritems(doc_margins)
                    if isinstance(v, numbers.Number)
                    and k in {'right', 'top', 'left', 'bottom'}
                }
                if doc_margins:
                    margin_top = margin_bottom = 0
                    page_margins = self.convert_page_margins(doc_margins)

        amap = json.loads(
            evaljs('''
        document.body.style.backgroundColor = "white";
        // Qt WebKit cannot handle opacity with the Pdf backend
        s = document.createElement('style');
        s.textContent = '* {opacity: 1 !important}';
        document.documentElement.appendChild(s);
        paged_display.set_geometry(1, %d, %d, %d);
        paged_display.layout();
        paged_display.fit_images();
        ret = book_indexing.all_links_and_anchors();
        window.scrollTo(0, 0); // This is needed as getting anchor positions could have caused the viewport to scroll
        JSON.stringify(ret);
        ''' % (margin_top, 0, margin_bottom)))

        if not isinstance(amap, dict):
            amap = {
                'links': [],
                'anchors': {}
            }  # Some javascript error occurred
        for val in itervalues(amap['anchors']):
            if isinstance(val, dict) and 'column' in val:
                val['column'] = int(val['column'])
        for href, val in amap['links']:
            if isinstance(val, dict) and 'column' in val:
                val['column'] = int(val['column'])
        sections = self.get_sections(amap['anchors'])
        tl_sections = self.get_sections(amap['anchors'], True)
        col = 0

        if self.header:
            evaljs('paged_display.header_template = ' +
                   json.dumps(self.header))
        if self.footer:
            evaljs('paged_display.footer_template = ' +
                   json.dumps(self.footer))
        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()

        def set_section(col, sections, attr):
            # If this page has no section, use the section from the previous page
            idx = col if col in sections else col - 1 if col - 1 in sections else None
            if idx is not None:
                setattr(self, attr, sections[idx][0])

        from calibre.ebooks.pdf.render.toc import calculate_page_number

        while True:
            set_section(col, sections, 'current_section')
            set_section(col, tl_sections, 'current_tl_section')
            self.doc.init_page(page_margins)
            num = calculate_page_number(self.current_page_num,
                                        self.opts.pdf_page_number_map, evaljs)
            if self.header or self.footer:
                if evaljs('paged_display.update_header_footer(%d)' %
                          num) is True:
                    self.load_header_footer_images()

            self.painter.save()
            mf.render(self.painter, mf.ContentsLayer)
            self.painter.restore()
            try:
                nsl = int(evaljs('paged_display.next_screen_location()'))
            except (TypeError, ValueError):
                break
            self.doc.end_page(nsl <= 0)
            if nsl <= 0:
                break
            evaljs(
                'window.scrollTo(%d, 0); paged_display.position_header_footer();'
                % nsl)
            if self.doc.errors_occurred:
                break
            col += 1

        if not self.doc.errors_occurred and self.doc.current_page_num > 1:
            self.doc.add_links(self.current_item, start_page, amap['links'],
                               amap['anchors'])
Esempio n. 7
0
 def run_for_a_time(self, timeout):
     final_time = time.time() + timeout
     loop = QEventLoop(self)
     while (time.time() < final_time):
         if not loop.processEvents():
             time.sleep(0.1)
Esempio n. 8
0
class PDFWriter(QObject):

    def _pass_json_value_getter(self):
        val = json.dumps(self.bridge_value)
        return val

    def _pass_json_value_setter(self, value):
        self.bridge_value = json.loads(unicode(value))

    _pass_json_value = pyqtProperty(str, 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

    @pyqtSlot(result=unicode)
    def tl_section(self):
        return self.current_tl_section

    def __init__(self, opts, log, cover_data=None, toc=None):
        from calibre.gui2 import must_use_qt
        must_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 = ''
        self.current_tl_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, opts=opts,
                             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 = 1.5 * opts._final_base_font_size
        if self.footer and opts.margin_bottom < min_margin:
            self.log.warn('Bottom margin is too small for footer, increasing it to %.1fpts' % min_margin)
            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 to %.1fpts' % min_margin)
            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, mi=pdf_metadata.mi)
        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()
                try:
                    p.loadFromData(self.cover_data)
                except TypeError:
                    self.log.warn('This ebook does not have a raster cover, cannot generate cover for PDF'
                                  '. Cover type: %s' % type(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_inline_toc(self):
        self.rendered_inline_toc = True
        from calibre.ebooks.pdf.render.toc import toc_as_html
        raw = toc_as_html(self.toc, self.doc, self.opts)
        pt = PersistentTemporaryFile('_pdf_itoc.htm')
        pt.write(raw)
        pt.close()
        self.render_queue.append(pt.name)
        self.render_next()

    def render_book(self):
        if self.doc.errors_occurred:
            return self.loop.exit(1)
        try:
            if not self.render_queue:
                if self.opts.pdf_add_toc and self.toc is not None and len(self.toc) > 0 and not hasattr(self, 'rendered_inline_toc'):
                    return self.render_inline_toc()
                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 bool(evaljs('''
                    window.mathjax.base = %s;
                    mathjax.check_for_math(); mathjax.math_present
                    '''%(json.dumps(mjpath, ensure_ascii=False)))):
            self.log.debug('Math present, loading MathJax')
            while not bool(evaljs('mathjax.math_loaded')):
                self.loop.processEvents(self.loop.ExcludeUserInputEvents)
            evaljs('document.getElementById("MathJax_Message").style.display="none";')

    def get_sections(self, anchor_map, only_top_level=False):
        sections = defaultdict(list)
        ci = os.path.abspath(os.path.normcase(self.current_item))
        if self.toc is not None:
            tocentries = self.toc.top_level_items() if only_top_level else self.toc.flat()
            for toc in tocentries:
                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']
                    sections[col].append(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('''
        Object.defineProperty(py_bridge, 'value', {
               get : function() { return JSON.parse(this._pass_json_value); },
               set : 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();
        window.scrollTo(0, 0); // This is needed as getting anchor positions could have caused the viewport to scroll
        '''%(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'])
        tl_sections = self.get_sections(amap['anchors'], True)
        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()

        def set_section(col, sections, attr):
            # If this page has no section, use the section from the previous page
            idx = col if col in sections else col - 1 if col - 1 in sections else None
            if idx is not None:
                setattr(self, attr, sections[idx][0])

        while True:
            set_section(col, sections, 'current_section')
            set_section(col, tl_sections, 'current_tl_section')
            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()
            try:
                nsl = int(evaljs('paged_display.next_screen_location()'))
            except (TypeError, ValueError):
                break
            self.doc.end_page(nsl <= 0)
            if nsl <= 0:
                break
            evaljs('window.scrollTo(%d, 0); paged_display.position_header_footer();'%nsl)
            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'])
Esempio n. 9
0
class PDFWriter(QObject):

    @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

    @pyqtSlot(result=unicode)
    def tl_section(self):
        return self.current_tl_section

    def __init__(self, opts, log, cover_data=None, toc=None):
        from calibre.gui2 import must_use_qt
        must_use_qt()
        QObject.__init__(self)

        self.logger = self.log = log
        current_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 = ''
        self.current_tl_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()

        def margin(which):
            val = getattr(opts, 'pdf_page_margin_' + which)
            if val == 0.0:
                val = getattr(opts, 'margin_' + which)
            return val
        ml, mr, mt, mb = map(margin, 'left right top bottom'.split())
        # 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)
        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, opts=opts,
                             mark_links=opts.pdf_mark_links, page_margins=(ml, mr, mt, mb))
        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 = 1.5 * opts._final_base_font_size
        if self.footer and mb < min_margin:
            self.log.warn('Bottom margin is too small for footer, increasing it to %.1fpts' % min_margin)
            mb = min_margin
        if self.header and mt < min_margin:
            self.log.warn('Top margin is too small for header, increasing it to %.1fpts' % min_margin)
            mt = 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, (mt, mb))
        self.margin_top, self.margin_bottom = map(lambda x:int(floor(x)), (mt, mb))

        self.painter = QPainter(self.doc)
        try:
            self.book_language = pdf_metadata.mi.languages[0]
        except Exception:
            self.book_language = 'eng'
        self.doc.set_metadata(title=pdf_metadata.title,
                              author=pdf_metadata.author,
                              tags=pdf_metadata.tags, mi=pdf_metadata.mi)
        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()
                try:
                    p.loadFromData(self.cover_data)
                except TypeError:
                    self.log.warn('This ebook does not have a raster cover, cannot generate cover for PDF'
                                  '. Cover type: %s' % type(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_inline_toc(self):
        evaljs = self.view.page().mainFrame().evaluateJavaScript
        self.rendered_inline_toc = True
        from calibre.ebooks.pdf.render.toc import toc_as_html
        raw = toc_as_html(self.toc, self.doc, self.opts, evaljs)
        pt = PersistentTemporaryFile('_pdf_itoc.htm')
        pt.write(raw)
        pt.close()
        self.render_queue.append(pt.name)
        self.render_next()

    def render_book(self):
        if self.doc.errors_occurred:
            return self.loop.exit(1)
        try:
            if not self.render_queue:
                if self.opts.pdf_add_toc and self.toc is not None and len(self.toc) > 0 and not hasattr(self, 'rendered_inline_toc'):
                    return self.render_inline_toc()
                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 bool(evaljs('''
                    window.mathjax.base = %s;
                    mathjax.check_for_math(); mathjax.math_present
                    '''%(json.dumps(mjpath, ensure_ascii=False)))):
            self.log.debug('Math present, loading MathJax')
            while not bool(evaljs('mathjax.math_loaded')):
                self.loop.processEvents(self.loop.ExcludeUserInputEvents)
            evaljs('document.getElementById("MathJax_Message").style.display="none";')

    def load_header_footer_images(self):
        from calibre.utils.monotonic import monotonic
        evaljs = self.view.page().mainFrame().evaluateJavaScript
        st = monotonic()
        while not evaljs('paged_display.header_footer_images_loaded()'):
            self.loop.processEvents(self.loop.ExcludeUserInputEvents)
            if monotonic() - st > 5:
                self.log.warn('Header and footer images have not loaded in 5 seconds, ignoring')
                break

    def get_sections(self, anchor_map, only_top_level=False):
        sections = defaultdict(list)
        ci = os.path.abspath(os.path.normcase(self.current_item))
        if self.toc is not None:
            tocentries = self.toc.top_level_items() if only_top_level else self.toc.flat()
            for toc in tocentries:
                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']
                    sections[col].append(toc.text or _('Untitled'))

        return sections

    def hyphenate(self, evaljs):
        evaljs(u'''\
        Hyphenator.config(
            {
            'minwordlength'    : 6,
            // 'hyphenchar'     : '|',
            'displaytogglebox' : false,
            'remoteloading'    : false,
            'doframes'         : true,
            'defaultlanguage'  : 'en',
            'storagetype'      : 'session',
            'onerrorhandler'   : function (e) {
                                    console.log(e);
                                }
            });
        Hyphenator.hyphenate(document.body, "%s");
        ''' % self.hyphenate_lang
        )

    def convert_page_margins(self, doc_margins):
        ans = [0, 0, 0, 0]

        def convert(name, idx, vertical=True):
            m = doc_margins.get(name)
            if m is None:
                ans[idx] = getattr(self.doc.engine, '{}_margin'.format(name))
            else:
                ans[idx] = m

        convert('left', 0, False), convert('top', 1), convert('right', 2, False), convert('bottom', 3)
        return ans

    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')
            if self.opts.pdf_hyphenate:
                self.paged_js += P('viewer/hyphenate/Hyphenator.js', data=True).decode('utf-8')
                hjs, self.hyphenate_lang = load_hyphenator_dicts({}, self.book_language)
                self.paged_js += hjs
            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()
        if self.opts.pdf_hyphenate:
            self.hyphenate(evaljs)

        margin_top, margin_bottom = self.margin_top, self.margin_bottom
        page_margins = None
        if self.opts.pdf_use_document_margins:
            doc_margins = evaljs('document.documentElement.getAttribute("data-calibre-pdf-output-page-margins")')
            try:
                doc_margins = json.loads(doc_margins)
            except Exception:
                doc_margins = None
            if doc_margins and isinstance(doc_margins, dict):
                doc_margins = {k:float(v) for k, v in doc_margins.iteritems() if isinstance(v, (float, int)) and k in {'right', 'top', 'left', 'bottom'}}
                if doc_margins:
                    margin_top = margin_bottom = 0
                    page_margins = self.convert_page_margins(doc_margins)

        amap = json.loads(evaljs('''
        document.body.style.backgroundColor = "white";
        // Qt WebKit cannot handle opacity with the Pdf backend
        s = document.createElement('style');
        s.textContent = '* {opacity: 1 !important}';
        document.documentElement.appendChild(s);
        paged_display.set_geometry(1, %d, %d, %d);
        paged_display.layout();
        paged_display.fit_images();
        ret = book_indexing.all_links_and_anchors();
        window.scrollTo(0, 0); // This is needed as getting anchor positions could have caused the viewport to scroll
        JSON.stringify(ret);
        '''%(margin_top, 0, margin_bottom)))

        if not isinstance(amap, dict):
            amap = {'links':[], 'anchors':{}}  # Some javascript error occurred
        for val in amap['anchors'].itervalues():
            if isinstance(val, dict) and 'column' in val:
                val['column'] = int(val['column'])
        for href, val in amap['links']:
            if isinstance(val, dict) and 'column' in val:
                val['column'] = int(val['column'])
        sections = self.get_sections(amap['anchors'])
        tl_sections = self.get_sections(amap['anchors'], True)
        col = 0

        if self.header:
            evaljs('paged_display.header_template = ' + json.dumps(self.header))
        if self.footer:
            evaljs('paged_display.footer_template = ' + json.dumps(self.footer))
        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()

        def set_section(col, sections, attr):
            # If this page has no section, use the section from the previous page
            idx = col if col in sections else col - 1 if col - 1 in sections else None
            if idx is not None:
                setattr(self, attr, sections[idx][0])

        from calibre.ebooks.pdf.render.toc import calculate_page_number

        while True:
            set_section(col, sections, 'current_section')
            set_section(col, tl_sections, 'current_tl_section')
            self.doc.init_page(page_margins)
            num = calculate_page_number(self.current_page_num, self.opts.pdf_page_number_map, evaljs)
            if self.header or self.footer:
                if evaljs('paged_display.update_header_footer(%d)'%num) is True:
                    self.load_header_footer_images()

            self.painter.save()
            mf.render(self.painter, mf.ContentsLayer)
            self.painter.restore()
            try:
                nsl = int(evaljs('paged_display.next_screen_location()'))
            except (TypeError, ValueError):
                break
            self.doc.end_page(nsl <= 0)
            if nsl <= 0:
                break
            evaljs('window.scrollTo(%d, 0); paged_display.position_header_footer();'%nsl)
            if self.doc.errors_occurred:
                break
            col += 1

        if not self.doc.errors_occurred and self.doc.current_page_num > 1:
            self.doc.add_links(self.current_item, start_page, amap['links'],
                            amap['anchors'])