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:
For our KDAB trainings, the students get a printed handout. The print shops printing the material require a PDF.
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
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:
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
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:
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?
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.
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.
3 - Oct - 2014
Beemaneni
can i have the source code of the above "Creating a PDF from a QtQuick 2 scene in SlideViewer"
How did you use QQuickImage? Is it not a private class? Can u brief me abit of how to use that?
5 - Oct - 2014
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.
7 - Oct - 2014
Beemaneni
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
7 - Oct - 2014
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.
8 - Oct - 2014
Beemaneni
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'
9 - Oct - 2014
Thomas McGuire
That error probably is because you need to include private/qquickitem_p.h as well.
13 - Oct - 2014
Beemaneni
Hi,
i did add that header file.but it still shows the same error.
13 - Oct - 2014
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.
13 - Oct - 2014
Beemaneni
Thanks Thomas
Your guidance was helpful.I would try myself to clear the error messages.
Thomas McGuire
Former KDAB employee
Thomas McGuire is a former KDAB employee
Jesper K. Pedersen
HR Director / COO
Jesper K. Pedersen – COO/HR director at KDAB. Jesper has actively developed with Qt since 1998 and, despite his fancy title, still does so.
He has held almost 100 training classes in Qt since 2000. Today, his greatest claim to fame is the QML youtube series and more recently his youtube series called Qt Widgets and More.
12 Comments
18 - Aug - 2014
John Layt
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.
3 - Oct - 2014
Beemaneni
can i have the source code of the above "Creating a PDF from a QtQuick 2 scene in SlideViewer"
5 - Oct - 2014
Thomas McGuire
See Jesper's comment an another blog post here: https://www.kdab.com/development-slideviewer-qml-based-presentation-program/#comments-section
3 - Oct - 2014
Beemaneni
How did you use QQuickImage? Is it not a private class? Can u brief me abit of how to use that?
5 - Oct - 2014
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.
7 - Oct - 2014
Beemaneni
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
7 - Oct - 2014
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.
8 - Oct - 2014
Beemaneni
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'
9 - Oct - 2014
Thomas McGuire
That error probably is because you need to include private/qquickitem_p.h as well.
13 - Oct - 2014
Beemaneni
Hi, i did add that header file.but it still shows the same error.
13 - Oct - 2014
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.
13 - Oct - 2014
Beemaneni
Thanks Thomas Your guidance was helpful.I would try myself to clear the error messages.