Skip to content

Creating a PDF from a QtQuick 2 scene in SlideViewer

The Challenge

Previously on this blog, we featured a series of articles about our QML-based presentation tool, SlideViewer. To quickly recap: SlideViewer is a presentation program that allows writing slides entirely in QML.

There are situations in which the slide deck needs to be available in PDF format in addition to the QML source:

  1. For our KDAB trainings, the students get a printed handout. The print shops printing the material require a PDF.
  2. For conferences such as Qt DevDays, conference organizers usually require speakers to send in their slide deck as PDF to make them available as download for everyone
  3. Sometimes the presentation is done on a machine without SlideViewer installed. In this case a PDF version is needed as a PDF viewer is usually available everywhere.

Had we used QtQuick 1, the task of creating a PDF from our QML-based slides would have been easy: QtQuick 1 uses raster-based painting via QPainter. With QPainter, it is simple to redirect rendering to QPrinter, which is able to use a PDF file as its output format.

QtQuick 2 uses an OpenGL scenegraph for rendering, and hence does not use QPainter and therefore does not offer a direct way to render the current scene to PDF.

So how did we solve the challenge of getting SlideViewer, which uses QtQuick 2, to generate a PDF file?

First Approach: Taking Screenshots

Even though one can not access the rendering of QtQuick 2 via QPainter redirection, there is a way to get the final rendered image of a slide: Taking a screenshot of a slide with QQuickWindow::grabWindow(). With that, the algorithm for creating a PDF is trivial:

 - For each slide
     - Display the slide in the main window
     - Take a screenshot with QQuickWindow::grabWindow()
     - Draw the screenshot into a QPrinter with QPainter::drawImage()

This approach however has a literally huge drawback: The file size of the PDF will be gigantic, as one big image per slide is stored in the PDF, instead of a few bytes for the text strings. A PDF of our training material with all optional topics contains much more than 1000 slides, and a PDF with screenshots of that weights several hundred megabytes. This size is obviously too impractical to handle. Furthermore the text in the PDF is not searchable and selectable.

Using screenshots for creating PDFs did not work, but is there another way?

Second Approach: Manual Painting

A Typical Slide

A Typical Slide

Looking more closely at a typical slide, it mainly consists of text and images – that is, the QtQuick Text and Image elements. There are a few non-visual elements as well, for example a Repeater for code snippets – which in the end only creates more Text elements though.

With the insight that a slide contains only 2 or 3 different QtQuick types, it should be possible to paint each item on a slide ourselves, with QPainter. How hard could that possibly be? We can simply iterate over the complete object tree of a slide, call QPainter::drawImage() when we encounter an Image, QPainter::drawText() when we encounter a Text and so on.

A (very simplified) version of our code that does exactly this looks like:

void SlidePrinter::printSlide(QQuickItem *slide)
{
    QPrinter pdf;
    ... // Initialize QPrinter
    QPainter painter;
    painter.begin(&pdf);
    paintItem(slide, &painter);
    painter.end();
}

void SlidePrinter::paintItem(QQuickItem *item, QPainter *painter)
{
    if (!item || !item->isVisible())
        return;

    painter->save();
    painter->setOpacity(item->opacity() * painter->opacity());

    QTransform transform;
    auto priv = static_cast<QQuickItemPrivate*>(QObjectPrivate::get(item));
    priv->itemToParentTransform(transform);
    painter->setTransform(transform, true /* combine */);

    if (item->clip()) {
        painter->setClipping(true);
        painter->setClipRect(QRectF{0, 0, item->width(), item->height()});
    }

    if (item->metaObject()) {
        painter->save();
        const QString className = item->metaObject()->className();
        if (className == "QQuickImage")
        paintImage(item, painter):
    } else if (className == "QQuickRectangle") {
        paintRectangle(item, painter):
    } else if (className == "QQuickText") {
        paintText(item, painter):
        painter->restore();
    }

    for (auto child : item->childItems())
        paintItem(child, painter);

    painter->restore();
}

One can see that besides simply painting each element, one needs to take care of per-item properties like position and clipping. For the former we used private API, QQuickItemPrivate::itemToParentTransform(). Not shown in the simplified version is that child items with negative z values need to be painted before their parent item.

Painting the elements

Now, painting an image is just a QPainter::drawImage() call, right?

void SlidePrinter::paintImage(QQuickImage *image, QPainter *painter)
{
    Qt::AspectRatioMode aspectRatioMode;
    switch (image->fillMode()) {
        case QQuickImage::Pad:
        case QQuickImage::Tile:
        case QQuickImage::TileHorizontally:
        case QQuickImage::TileVertically:
        case QQuickImage::Stretch: aspectRatioMode = Qt::IgnoreAspectRatio; break;
        case QQuickImage::PreserveAspectFit: aspectRatioMode = Qt::KeepAspectRatio; break;
        case QQuickImage::PreserveAspectCrop: aspectRatioMode = Qt::KeepAspectRatioByExpanding; break;
    }

    const QImage original = image->image();
    QSizeF targetSize = original.size();
    targetSize.scale({image->width(), image->height()}, aspectRatioMode);
    QRectF targetRect{0, 0, targetSize.width(), targetSize.height()};
    QRectF sourceRect({0, 0}, original.size());

    if (image->fillMode() == QQuickImage::PreserveAspectCrop) {
        if (targetRect.height() > image->height()) {
            const qreal visibleHeightPercentage = image->height() / targetRect.height();
            const qreal visibleSourceHeight = sourceRect.height() * visibleHeightPercentage;
            const qreal sourceOffset = (sourceRect.height() - visibleSourceHeight) / 2;
            sourceRect.setY(sourceOffset);
            sourceRect.setHeight(visibleSourceHeight);
            targetRect.setHeight(image->height());
        } else if (targetRect.width() > image->width()) {
            ...
        }
    }

    if (image->fillMode() == QQuickImage::PreserveAspectFit) {
        if (targetRect.width() < image->width()) {
            const int space = image->width() - targetRect.width();
            targetRect.translate(space / 2, 0);
        } else if (targetRect.height() < image->height()) {
            ...
        }
    }

    QImage copy({(int)targetRect.width(), (int)targetRect.height()}, QImage::Format_ARGB32);
    copy.fill({0, 0, 0, 0});
    QPainter imagePainter(&copy);
    imagePainter.setOpacity(painter->opacity());
    imagePainter.setRenderHint(QPainter::SmoothPixmapTransform, true);
    imagePainter.drawImage({0, 0, targetRect.width(), targetRect.height()}, original, sourceRect);
    imagePainter.end();

    painter->drawImage(targetRect.x(), targetRect.y(), copy);
}

Turns out it is a bit more complicated than that, as the various different fill modes require a bit of math. That math already exists in QQuickImage::updatePaintNode(), but since we’re rolling our own rendering code, we need to duplicate everything that the QtQuick rendering code does. It turns out that the rendering code actually does a lot, and duplicating everything on our own would be quite a bit of effort. Because of that, we didn’t implement all features of QtQuick and left out, for example, the various tiling modes – we were not using them, and they are much harder to implement in QPainter than in OpenGL.

The Text and Rectangle elements are handled in a similar way.

In the above code snippet, I cheated a bit for simplicity’s sake: There is no QQuickImage::fillMode() method. This can however easily be replaced by calling QObject::property("fillMode"). For generating the PDF, using private API or even finding workarounds for missing private API is something we had to do a lot – QtQuick items aren’t meant to be accessed from the C++ side after all.

Screenshots

What if the slide contains QtQuick elements that are not text, images or rectangles? We do actually have some examples of that, one case is that we display a QtQuick Controls Slider in our QtQuick Controls introduction section. In cases like these, we make an exception and grab a screenshot of just that element, and use QPainter::drawImage() to add the screenshot to the PDF. We only have a handful of such cases, using a screenshot as a fallback in these cases is a reasonable compromise.

Conclusion

The above approach of manually rendering text, images and rectangles with QPainter to generate a PDF works surprisingly well, for our use-case, as it handles thousands of slides without problems.

There are several drawbacks that makes this approach a bit less than perfect for being a generic QPainter-based renderer for QtQuick 2 applications:

  • It uses private API of QtQuick
  • All of the scenegraph rendering code has to be duplicated into QPainter rendering code
  • It only works for a fixed subset of QtQuick types, other types will not be supported
  • It still relies on QQuickWindow, especially to do the polishing of items, for taking screenshots of unsupported types and of course for event handling

So this approach might not be the perfect approach for making QtQuick 2 run on systems without OpenGL, or would at least require some additional effort like duplicating most of the QQuickWindow code.

However, for our use-case of creating PDFs from slides, the above solution works just fine and creates nice small and searchable PDFs.

12 thoughts on “Creating a PDF from a QtQuick 2 scene in SlideViewer”

  1. Hmmm, that gets me thinking about how to abstract the QPdfEngine code so it’s directly usable from QML… Which paired with the future PDF/XPS based print work-flow would give us QML printing as well.

  2. can i have the source code of the above “Creating a PDF from a QtQuick 2 scene in SlideViewer”

  3. How did you use QQuickImage? Is it not a private class? Can u brief me abit of how to use that?

    1. Thomas McGuire

      Indeed, QQuickImage is a class that is not exported.

      However, its base class, QQuickImageBase, is exported and can be used when using “QT += quick-private” in the .pro file and including “private/qquickimagebase_p.h”. QQuickImageBase contains most of the useful methods, for example source().

      Still, some methods are only present in QQuickImage. One example is the “fillMode” property. Although there is no way to access the non-exported method “fillMode()”, it is possible to use QObject::property() to access the fillMode, e.g. “property(“fillMode”).toInt()”.

      Same situation with QQuickRectangle.

  4. Hi ,
    i get the following error when i compile it.Can you help me to sort this.
    error: incomplete type ‘QObjectPrivate’ used in nested name specifier
    auto priv = static_cast(QObjectPrivate::get(item));
    ^

    Thanks in advance
    Bala Beemaneni

    1. Thomas McGuire

      The compiler is telling you that it doesn’t know what “QObjectPrivate” is. That is a private class which can be found in the header “private/qobject_p.h”, so include that header first.

  5. hi,
    Thanks for the help.That worked out.But following that ,now i get the type casting error as below

    invalid static_cast from type ‘QObjectPrivate*’ to type ‘QQuickItemPrivate*’

    1. Thomas McGuire

      Interesting, it works for me here.

      From just the one line of code and the error message, I can not remotely diagnose your problem. Please try digging into the cause of the error yourself.

Leave a Reply

Your email address will not be published. Required fields are marked *