1

I am trying to make an application that displays a PDF file, using PyQt4 and python-poppler-qt4.

So far I have managed to display the entire document by loading pixmaps generated with Poppler, set on a QLabel and appended to a QFrame. The QFrame is displayed in a QScrollArea.

It looks pretty good, until implementing zooming, which is done by regenerating the pixmaps all over again, with an incremented resolution. This process requires the entire document to be rendered into pixmaps, which obviously takes time and results into an unwanted lag.

Logic wants that I should display images of the pages I am seeing only (it sounds like quantum physics). I have two options in mind:

  1. create blank pages with QLabels and load the image onto them when they become visible in the scroll area;
  2. create only one page and add or remove precedent or subsequent pages right before it should be displayed.

I am not sure I am on the right track or whether there is an alternative.

The first option seems more feasible, because the visibility of a blank page determines when the pixmap has to be uploaded (although I have no idea how to delete that pixmap when the page is hidden). Yet I am not sure that zooming will be faster this way, since a document of, say, 600 pages, will have to be regenerated, albeit with blank pages.

The second option should definitely improve zooming since 1 to 4 pages at a time would have to be regenerated when zooming. In that second case however, I am not sure how to trigger the construction of pages.

What would you suggest?

neydroydrec
  • 6,973
  • 9
  • 57
  • 89

2 Answers2

1

wouldn't it be simple to forget the QLabels and directly draw the image:

from PyQt4.QtGui import *
import sys

app = QApplication(sys.argv)

class Test(QWidget):

    def __init__(self):
        super(Test, self).__init__()
        self.painter = QPainter()
        # placeholder for the real stuff to draw
        self.image = QImage("/tmp/test.jpg")

    def paintEvent(self, evt):
        rect = evt.rect()
        evt.accept()
        print rect
        self.painter.begin(self)
        zoomedImage = self.image   # ... calculate this for your images
        sourceRect = rect          # ... caluclate this ...
        # draw it directly
        self.painter.drawImage(rect, self.image, sourceRect)
        self.painter.end()


t = Test()
t.setGeometry(0,0,600,800)

s = QScrollArea()
s.setWidget(t)

s.setGeometry(0,0,300,400)
s.show()
app.exec_()
mata
  • 67,110
  • 10
  • 163
  • 162
  • Thanks. I've seen code using Painter() before, and it keeps evading my understanding. So please bear with me. Does the painter paints on its parent, Test()? With multiple pages, each should be a Test()-like widget, stacked vertically onto a QFrame(), which is set in QScrollArea? Does the paintEvent() triggers every time the page or part of the page emerges? Do I understand what you propose correctly if I say it is drawing the images progressively as they appear? – neydroydrec May 16 '12 at 17:11
  • which component the painter paints on is set in the call to it's `begin` method, so in this case it's the `QWidget` (`self`). `paintEvent` is called every time the component has to be repainted, and `evt.rect()` gives you the area that has to be repaintend, so you only have to paint the visible part. i don't know how you layout is supposed to look like, so i can't realy tell you how to do it exactly. – mata May 16 '12 at 17:15
  • It works when a single page (which is a self-painting QWidget as you introduced above) is the QScrollArea's main widget. If I append many such pages on a QFrame and a QVBoxLayout, then the paintEvent of the pages is not triggered and I cannot see any. How is this possible? – neydroydrec May 17 '12 at 06:03
  • the painter just allows you to paint on the canvas of the widget, it does not affect the rendering of subcomponents you add to it's layout. i was just proposing this as a way to decide what has to be displayed (setting the widget's size to the combined size of the images and draw only the visible part). if you want something more complex, you could have a look at the [graphics view examples](http://qt-project.org/doc/examples-graphicsview.html) – mata May 17 '12 at 07:40
  • indeed I cam across QGraphicsView, though I haven't found how to apply a layout to it. – neydroydrec May 17 '12 at 07:50
1

I've worked out an answer, using option 1 in the question:

def moveEvent(self, event):
    self.checkVisibility()
    event.ignore()

def resizeEvent(self, event):
    self.checkVisibility()
    event.ignore()

def checkVisibility(self):
    print "Checking visibility"
    for page in self.getPages():
        if not page.visibleRegion().isEmpty():
            if page.was_visible:
                pass
            else:
                print page.page_number, "became visible"
                page.was_visible = True
                self.applyImageToPage(page)
        else:
            if page.was_visible:
                print page.page_number, "became invisible"
                page.was_visible = False
            else:
                pass
def applyImageToPage(self, page):
    print "applying image to page", page.page_number
    source = self.getSourcePage(self.getPageNumber(page))
    scale = self.display.scale
        # this is where the error occurs
    image = source.renderToImage(72 * scale, 72 * scale)
    pixmap = QtGui.QPixmap.fromImage(image)
    page.setPixmap(pixmap)
neydroydrec
  • 6,973
  • 9
  • 57
  • 89
  • Option two is less efficient since it requires to reload the same pixmaps. The above code should be improved upon using a thread instead, to avoid lag in scrolling. – neydroydrec May 21 '12 at 03:55
  • PS: threading in Qt4 is not working well when doing graphics. The thread produces buggy images or crashes the application. Perhaps I did not to it well enough. In any case, the delay scrolling is mostly the result of large DPI. Hence if image is re-loaded according to zoom-level, then the amount to process per visible area is light enough not to prevent smooth scrolling. – neydroydrec May 23 '12 at 06:55