Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
Image asynchronous loading with QIODevice subclass

This feature was created to handle the rendering case in Qt PDF:

  • we want to be able to use an ordinary Image to render a PDF page (PDF is treated as an image format, like SVG)
  • in PdfMultiPageView, the images are used in TableView delegates, so the user is free to scroll through a lot of pages, triggering a lot of rendering work very quickly
  • loading is asynchronous to keep the scrolling "fluid"
  • pages that we began rendering might not need to continue, if the user quickly scrolls them out of view again
  • the user can also change the zoom level, which needs to trigger re-rendering
  • the user can load a different PDF file: pages from the previous one might still be rendering, but those are now irrelevant. At the very least, it should not crash in this situation.
  • PDFium is not reentrant: we have only one worker thread to do all the page rendering, to avoid crashing.
  • QPdfIOHandler is a subclass of QImageIOHandler.
  • QImageIOHandler::setDevice() sets the expectation that it loads from an open file. But it's a nice optimization to share the QPdfDocument object with the QImageIOHandler.
  • QPdfFile is the means by which QPdfIOHandler receives the QPdfDocument object. It's a subclass of QIODevice so that it can "carry" some extra information along when it's passed to QPdfIOHandler::setDevice() (the document object).
  • So in realistically full-featured applications, we need a PdfPageImage after all, just to add API to bind the QQuickPdfDocument which may have already been needed for other reasons. But a plain Image can also work: it comes at the cost that QPdfIOHandler::load() needs to make its own instance of QPdfDocument.

Multi-threading tends to lead to race conditions, in general. But the Image.asynchronous API blithely sets the expectation that loading of any image format can be threaded, to avoid UI stuttering. For most image formats, reading consists of nothing more than reading from a file or network connection, and straightforward format interpretation. But since PDF is treated as an image format (in spite of its complexity), we need to at least support the use of one worker thread to separate PDFium's work from the UI thread. But the work may need to be interrupted when a page-rendering job is no longer relevant, due to delegates scrolling out of view, change in zoom level, closing the document etc.

Here are some details about how we handle opening a different document while pages from the previous document are still being rendered:

QQuickPdfPageImage::load() calls ② carrierFile() to get a ③ QPdfFile instance (a subclass of QIODevice), and calls ④ QQuickPixmap::loadImageFromDevice() which ⑤ saves the device to QQuickPixmapData::specialDevice, a QPointer. Then it calls ⑥ QQuickPixmapReader::startJob() which ⑦ posts a ProcessJobs event. The worker thread ⑪ handles it in QQuickPixmapReader::processJob() and calls ⑫ readImage(), passing a simple QIODevice pointer extracted from the specialDevice QPointer. readImage() relies on that pointer being valid until the work is done. This work includes calling ⑬ QPdfIOHandler::load(), which gets the QPdfDocument pointer from QPdfFile::document() and then calls ⑮ QPdfFile::render().

The rendering job may be cancelled at any time: the user may continue scrolling through delegates in a TableView or ListView, or may load a different document. In the latter case, ⑨ QQuickPdfDocument::setSource() calls ⑩ deleteLater() on the same QPdfFile instance that is being stored in QQuickPixmapData::specialDevice. If the rendering job has not yet started, QQuickPixmapReader::processJob() detects that either deleteLater() has been called or the QPointer is already null, and cancels the job. But what if it happens while the rendering job is already running? In practice, we do not see a crash in this case. But for the sake of prevention, if we change the thread affinity of specialDevice to the rendering job's thread, we can ensure that it cannot happen until after readImage() is done (it doesn't return to the event loop until it's done). This requires cooperation because QObject::moveToThread()

can only "push" an object from the current thread to another thread, it cannot "pull" an object from any arbitrary thread to the current thread.

The expectation is that QPdfFile's constructor calls moveToThread(nullptr). If that has been done, QQuickPixmapReader::processJob() can see that specialDevice->thread() == nullptr, and can change the affinity to its own thread. Otherwise, it crosses its fingers and leaves the thread affinity as-is.