Insights

Power of Qt - Making a PDF Viewer Desktop Application in a Few Hours

Recently, we had a client come to use that had a unique request. They wanted to programmatically read a PDF file, find a barcode contained in it, read the barcode and get the value it represented and then rename the file to match the value contained in the bar code. Scanning a physical barcode and getting the value is child’s play. But reading it out of a file? This was tricky business, and one we weren’t sure we could pull off for a reasonable time and cost at first glance.

We’ve been working with the Qt technology framework since the year 2000, and it is our natural ‘go to’ for any project to see if it can solve our problem. At its heart, it is a multi-platform windowing toolkit for C++, but over the years there have been many modules added that extend beyond just the presentation layer and with bindings to other languages. It is mature with a robust community that keeps surprising us with the solutions they come up with.

What we want to do in this article is show an example of developing a complex application with minimal effort, that is combining elements of both desktop application programming and web programming.

Our application needed to work in batch mode. It was going to be setup in the Windows scheduler and process a directory of files whenever it ran. We built all the normal stuff into it to deal with missing directories, flexible paths, duplicate files and such, but that was simple, it was reading the PDF file and finding/interpreting the barcode that was our challenge.

Initially, we looked at the ImageMagic conversion utility, but it flat didn’t do what we needed it to do. We looked around and found a lot of tools, but all of them were missing one thing or another, like batch mode, or the license was a problem. Since a ready-made solution wasn’t at hand, we decided to see what we could homebrew.

Since Qt was a mature technology, and it is trivial to create a PDF document with the QPrinter functionality, we wondered if there was a way to reverse the process and turn the PDF into an image file. Remembering PDF.js as another piece of mature software that does PDF rendering, we questioned if there was a way to merge these technologies to get to our solution. After some tinkering, we found the Qt component QWebEngineView that solves our problem. Let’s take a look at how the code works:

Based on the QMainWindow class:

m_webView = new QWebEngineView(this); m_webView->load(url); setCentralWidget(m_webView); 

This will present a window that contains a webpage inside. Now let's have a look at PDF.js. The project is quite large and presents a heavy footprint, but there is an option to build a minfied version that can easily be included into other websites. Here is how we retrieved and built it:

$ git clone git://github.com/mozilla/pdf.js.git $ cd pdf.js $ brew install npm $ npm install -g gulp-cli $ npm install $ gulp minified 

The result is a “compiled” version of pdf.js in the folder build/minified, then we copy it to our project folder. Now set the url to point to the local file minified/web/viewer.html
auto url = QUrl::fromLocalFile(app_path+"/minified/web/viewer.html");

Now build and run:

This worked perfectly right out of the box, so our concept is valid. However it is showing their example PDF file, so how do we bypass the filename and inject our own into the javascript engine?

There is another clever bit of Qt technology called QWebChannel. The idea is that on C++/Qt side we instantiate QWebChannel object and set this channel to a webpage. With that channel, we can register objects that can be accessed from a JavaScript scope:

auto url = QUrl::fromLocalFile(app_path+"/minified/web/viewer.html"); m_communicator = new Communicator(this); m_communicator->setUrl(pdf_path);m_webView = new QWebEngineView(this); QWebChannel * channel = new QWebChannel(this); channel->registerObject(QStringLiteral("communicator"), m_communicator); m_webView->page()->setWebChannel(channel); m_webView->load(url); setCentralWidget(m_webView);

The above code allows us to access the communicator object from JavaScript. Now we need to make some changes to viewer.html/viewer.js and add qwebchannel.js to allow communication on other side, but that is simple enough:

1. For viewer.html we just add a reference to qwebchannel.js

<script src="qwebchannel.js"></script>

2. For viewer.js we add the initialization of QWebChannel and bypass the filename just below the definition of the original url pointing to that example pdf file:


var DEFAULT_URL = 'compressed.tracemonkey-pldi-09.pdf';
new QWebChannel(qt.webChannelTransport
,function(channel) {
var comm = channel.objects.communicator;
DEFAULT_URL = comm.url;

 

var DEFAULT_URL = 'compressed.tracemonkey-pldi-09.pdf'; new QWebChannel(qt.webChannelTransport ,function(channel) { var comm = channel.objects.communicator; DEFAULT_URL = comm.url; 

Here is the secret sauce in how this all works. Before page loading, we attach a web channel and register the communicator object. Then when viewer.html is loaded for the first time, we have a defined QWebChannel JS class. Just after the declaration of DEFAULT_URL, we create JS QWebChannel. Once it is instantiated and communication is established, an attached function is called which reads the url from communicator object. This url is used instead of the example pdf file.

When the PDF.js code changes are done, just rebuild the minified version:

$ gulp minified

Now we copy the minified to our project home. From here, we can make changes like allowing the application to accept command line arguments or profile a list of available PDF files that you want to process – whatever it is you need. This formed the core of our project for the client because we could now deal with the information inside the PDF much easier.

And there you have it, a completed PDF viewer desktop application just in few hours (not counting research).

Project github: https://github.com/yshurik/qpdfjs

Article written by Shawn Gordon and Oleksandr Iakovliev
Image credit by Qt
Want more? For Job Seekers | For Employers | For Influencers