Ian Byrd


Resource interception in Qt WebKit

TL;DR:


Oh boy, I’d been dancing around Qt WebKit 5.1 for a couple of days already, when I finally found a way to implement sort of a trivial thing. The original task was about saving assets from the WebKit view. Suppose you’ve got a web browser in Qt and you want it to automatically save all, let’s say, pictures, on the hard drive. It seems like a trivial thing though: traversing through all the QWebElement objects by img selector and point its content with QWebElement::render() on QImage, which gets saved eventually. Unfortunately, it lets you capture static images only (gifs are not gonna work) and it obviously won’t work for any other assets (styles, scripts). So we obviously should go the other way around. Turns out, it’s not that easy at all. I’ll tell you about the path I went through to complete the objective and get the whole thing working.

Problem

Since my particular usecase was specific enough (I was interested in .png images only), I’ve implemented an algorithm from the previous paragraph and tried it out with Qt 5.1 on Mac. All in all, it didn’t work at all. All the saved pictures ended up being either black rectangles or just random rubbish. At this point I recalled a shipped Qt 5.1 example, implementing identical functionality. It surprisingly didn’t work neither on Mac nor Linux. No worries, let’s move on.

So what’s next? No way, it just doesn’t work and that’s it. I didn’t have any time to play Hide-and-Seek with webkit, so I’ve started looking for alternatives. Here is the funniest one: I’ve been drawing an image on HTML5 canvas, encoding what’s been drawn in base64 and eventually decoding it on the back-end side. You guess what? It didn’t work as well: images were either black or broken pixmaps. Deffo crazy, but I ain’t got any time to work it around—deadline is on fire!

Solution

What if we could intercept all the assets loaded? You can’t do that out of the box, QNAM (which is used for networking by QWebView) just won’t let you do that. Well, it would, but the long way around. I created the InterceptorManager class, directly inheriting the network access manager:

class InterceptorManager : public QNetworkAccessManager
{
	Q_OBJECT
public:
	explicit InterceptorManager(QObject *parent = 0);

protected:
	QNetworkReply *createRequest(Operation op, const QNetworkRequest &r, QIODevice *d)
	{
		QNetworkReply *real = QNetworkAccessManager::createRequest(op, r, d);
		if (r.url().toString().endsWith(".png")) {
			// a tricky one
			NetworkReplyProxy *proxy = new NetworkReplyProxy(this, real);
			return proxy;
		}

		return real;
	}
};

Let’s override protected createRequest() and return a custom QNetworkReply proxy for all the assets of our interest. Why would we do that? QNetworkReply is not a sequential device, thus you obviously can’t read its data twice and QWebView must read the image anyway, so it’s able to render one. As you see, we won’t have much to read. That said, there are no implementations of sequential network replies in Qt. We gotta write one then!

I wouldn’t say that writing a proxy for QNetworkReply is trivial: original implementation is quite big and covers a lot, so implementing it might take some time. Hopefully, in your case it won’t, since I’ve uploaded it to GitHub: tucnak/qtwebkit-ri. I’m not sure if it’s entirely correct, but it works. All in all, that’s what really matters, doesn’t it?

Have fun!