📜 ⬆️ ⬇️

Synchronous and asynchronous download images from the network with subsequent processing

Good to all habradnya!

Today I want to talk about one of the methods of synchronous and asynchronous image download from the network. To prevent the article from being boring, we will try to process the downloaded images in some way using Qt.


How are we going to upload?


To upload images, we will use QNetworkAccessManager and QEventLoop , as well as some meta objects. We will upload an HTTP image in any format supported by Qt. Well, we will still handle redirects.
')

How we will process?


There is a wonderful QGraphicsEffect class with subclasses. But we will not work with them in this article, accept! And I will even explain why. For example, in Qt 4.8.0, these effects lead to crashing of the application in Mac OS X 10.7. +, And in Qt 4.7.4 they do not work at all on the same system. I don’t know how it happened, but I put the bug in the Qt bug tracker.

So, we will create our own class for image processing. He will be able to do the following:

Immediately, I note that the full code of the test project can be downloaded on a githab, a link at the end of the article.

So, loading image


To begin, we define what we want. And we want this: we call a certain method of a certain class, transfer the image URL to it, as well as to what object to transfer the received image and to which method. And when the image is loaded, our class should call the desired method of the desired object and transfer the downloaded image to it. And all this is asynchronous. Sounds good?

For the cause! We create the Networking class (I made it static, but it does not play a big role), and we create the NetworkingPrivate class - for real work.

 // networking.h class Networking { public: static QImage httpGetImage(const QUrl& src); static void httpGetImageAsync(const QUrl& src, QObject * receiver, const char * slot); private: static NetworkingPrivate * networkingPrivate; static void init(); } // networking_p.h class NetworkingPrivate : public QObject { Q_OBJECT public: NetworkingPrivate(); ~NetworkingPrivate(); QImage httpGetImage(const QUrl& src) const; void httpGetImageAsync(const QUrl& src, QObject * receiver, const char * slot); public slots: void onFinished(QNetworkReply* reply); private: QNetworkAccessManager * nam; QEventLoop * loop; QMap<QNetworkReply*, QPair<QObject*, QPair<QUrl, const char *> > > requests; }; 

Actually, our class will be able to load pictures both synchronously and asynchronously. So there is a choice.

Example of use:

 // myclass.h class MyClass: public QObject { // ... public slots: void loadImage(const QString & urlString); void onImageReady(const QUrl& url, const QImage & image); } // myclass.cpp void MyClass::loadImage(const QString & urlString) { Networking::httpGetImageAsync(QUrl(urlString), this, "onImageRead"); } 

A few explanations about the insane private class. QNetworkAccessManager we need to send http-requests, QEventLoop - to wait for a response in the case of synchronous requests, and this horror QMap<QNetworkReply*, QPair<QObject*, QPair<QUrl, const char *> > > requests - to store all requests to know which picture to which object should be delivered after loading.

Now the most interesting thing is the implementation of private class functions (the Networking class, as you guessed, only redirects calls to its private class).

 NetworkingPrivate::NetworkingPrivate() { nam = new QNetworkAccessManager(); loop = new QEventLoop(); connect(nam, SIGNAL(finished(QNetworkReply*)), loop, SLOT(quit())); connect(nam, SIGNAL(finished(QNetworkReply*)), SLOT(onFinished(QNetworkReply*))); } NetworkingPrivate::~NetworkingPrivate() { nam->deleteLater(); loop->deleteLater(); } QImage NetworkingPrivate::httpGetImage(const QUrl& src) const { QNetworkRequest request; request.setUrl(src); QNetworkReply * reply = nam->get(request); loop->exec(); QVariant redirectedUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute); QUrl redirectedTo = redirectedUrl.toUrl(); if (redirectedTo.isValid()) { // guard from infinite redirect loop if (redirectedTo != reply->request().url()) { return httpGetImage(redirectedTo); } else { qWarning() << "[NetworkingPrivate] Infinite redirect loop at " + redirectedTo.toString(); return QImage(); } } else { QImage img; QImageReader reader(reply); if (reply->error() == QNetworkReply::NoError) reader.read(&img); else qWarning() << QString("[NetworkingPrivate] Reply error: %1").arg(reply->error()); reply->deleteLater(); return img; } } void NetworkingPrivate::httpGetImageAsync(const QUrl& src, QObject * receiver, const char * slot) { QNetworkRequest request; request.setUrl(src); QPair<QObject*, QPair<QUrl, const char *> > obj; obj.first = receiver; obj.second.first = src; obj.second.second = slot; QNetworkReply * reply = nam->get(request); requests.insert(reply, obj); } void NetworkingPrivate::onFinished(QNetworkReply* reply) { if (requests.contains(reply)) { QPair<QObject*, QPair<QUrl, const char *> > obj = requests.value(reply); QVariant redirectedUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute); QUrl redirectedTo = redirectedUrl.toUrl(); if (redirectedTo.isValid()) { // guard from infinite redirect loop if (redirectedTo != reply->request().url()) { httpGetImageAsync(redirectedTo, obj.first, obj.second.second); } else { qWarning() << "[NetworkingPrivate] Infinite redirect loop at " + redirectedTo.toString(); } } else { QImage img; QImageReader reader(reply); if (reply->error() == QNetworkReply::NoError) reader.read(&img); else qWarning() << QString("[NetworkingPrivate] Reply error: %1").arg(reply->error()); if (obj.first && obj.second.second) QMetaObject::invokeMethod(obj.first, obj.second.second, Qt::DirectConnection, Q_ARG(QUrl, obj.second.first), Q_ARG(QImage, img)); } requests.remove(reply); reply->deleteLater(); } } 

We now analyze these functions. In the constructor, we create a QEventLoop , a QNetworkAccessManager and connect the signal to complete the request with QEventLoop::quit() and our onFinished method.

For synchronous download, we execute the request and run the Event Loop, which will be completed at the end of the download. At the same time, we still check the redirect and its obsession, so that the user can enter any links to the images, including those passed through the link shortening.

Well, when we got our picture, we launch the QImageReader and read the data into the final QImage , which we return.

With asynchronous loading, everything is trickier. We save the request (more precisely, the link to the answer), the target object and its method in our terrible QMap , and then run the request. And at the end of the request, we do the same thing as with a synchronous request (checking for redirect, its cyclicality and reading the picture), but the resulting QImage passed to the target object using QMetaObject::invokeMethod . As parameters - URL request and image.

Well, you can make a simple form in which you can drive a URL, click on the button and get an image from the network. Synchronously or asynchronously. And rejoice.

But we will go further, and the resulting image will change.

Image processing class


We create another class (I have it again static, although for no reason at all), let's call it ImageManager . And we will have the following methods in it:

 class ImageManager { public: static QImage normallyResized(const QImage & image, int maximumSideSize); static QImage grayscaled(const QImage & image); static QImage squared(const QImage & image, int size); static QImage roundSquared(const QImage & image, int size, int radius); static QImage addShadow(const QImage & image, QColor color, QPoint offset, bool canResize = false); static QImage colorized(const QImage & image, QColor color); static QImage opacitized(const QImage & image, double opacity = 0.5); static QImage addSpace(const QImage & image, int left, int top, int right, int bottom); static QImage rotatedImage(const QImage & image, qreal angle); static QColor resolveColor(const QString & name); }; 

It will allow us to get something like this (see the test project at the end of the article):



Let's go in order. The first method is the least interesting, it only normalizes the image size on the maximum side. The third method is also not particularly interesting - it makes the image square (adjusting the size and cutting off the excess). I will not even include their sources in the article.

Next will go quite interesting.

Grayscale.


I found even two ways to do it, but I haven’t bothered to test both for speed yet. So I bring two to choose from.

The first way is to convert the image to the QImage :: Format_Indexed8 format, which means converting the image into an 8-bit indexable color. To do this, you need to create a "color map" of 256 elements - from white to black.

 QImage gray(image.size(), QImage::Format_ARGB32); gray.fill(Qt::transparent); static QVector<QRgb> monoTable; if (monoTable.isEmpty()) { for (int i = 0; i <= 255; i++) monoTable.append(qRgb(i, i, i)); } QPainter p(&gray); p.drawImage(0, 0, image.convertToFormat(QImage::Format_Indexed8, monoTable)); p.end(); return gray; 

The second method is based on direct work with image bits. Pass through all the pixels and set them to the value of gray.

 QImage img = image; if (!image.isNull()) { int pixels = img.width() * img.height(); if (pixels*(int)sizeof(QRgb) <= img.byteCount()) { QRgb *data = (QRgb *)img.bits(); for (int i = 0; i < pixels; i++) { int val = qGray(data[i]); data[i] = qRgba(val, val, val, qAlpha(data[i])); } } } return img; 

The second method, in my opinion, should work faster, since no additional image is created. In addition, it is also suitable for images with transparency, which is also very good. That is why it is used in the finals.

Corner rounding


Here the algorithm is quite interesting. My first thought was to create a mask and crop an image on it. But after long unsuccessful attempts to correctly draw this very mask with QPainter :: draw [Ellipse | Arc | RoundedRect | Path], I abandoned this idea. For some reason, this approach gives a good result only for some rounding radii. In addition, the result may be different in different operating systems, which also does not honor this method. This, apparently, is due to the inability to make antialiasing for the bit mask - it should have only two colors, black and white. The new method laughs at these problems, and gives an additional bun in the form of smooth rounding with anti-aliasing.

 QImage shapeImg(QSize(size, size), QImage::Format_ARGB32_Premultiplied); shapeImg.fill(Qt::transparent); QPainter sp(&shapeImg); sp.setRenderHint(QPainter::Antialiasing); sp.setPen(QPen(Qt::color1)); sp.setBrush(QBrush(Qt::color1)); sp.drawRoundedRect(QRect(0, 0, size, size), radius + 1, radius + 1); sp.end(); QImage roundSquaredImage(size, size, QImage::Format_ARGB32_Premultiplied); roundSquaredImage.fill(Qt::transparent); QPainter p(&roundSquaredImage); p.drawImage(0, 0, shapeImg); p.setCompositionMode(QPainter::CompositionMode_SourceIn); p.drawImage(0, 0, squared(image, size)); p.end(); return roundSquaredImage; 

The essence is almost the same as the image masking. Create a rounded black square (with anti-aliasing), and then draw on top of it the original image with the composition mode QPainter::CompositionMode_SourceIn . Simple and tasteful, as they say.

Add shadow


Now try to add a shadow to the image. And, taking into account transparency. The resulting picture, of course, may have dimensions that are different from the original.

 QSize shadowedSize = image.size(); if (canResize) { shadowedSize += QSize(qAbs(offset.x()), qAbs(offset.y())); } QImage shadowed(shadowedSize, QImage::Format_ARGB32_Premultiplied); shadowed.fill(Qt::transparent); QPainter p(&shadowed); QImage shadowImage(image.size(), QImage::Format_ARGB32_Premultiplied); shadowImage.fill(Qt::transparent); QPainter tmpPainter(&shadowImage); tmpPainter.setCompositionMode(QPainter::CompositionMode_Source); tmpPainter.drawPixmap(QPoint(0, 0), QPixmap::fromImage(image)); tmpPainter.setCompositionMode(QPainter::CompositionMode_SourceIn); tmpPainter.fillRect(shadowImage.rect(), color); tmpPainter.end(); QPoint shadowOffset = offset; if (canResize) { if (offset.x() < 0) shadowOffset.setX(0); if (offset.y() < 0) shadowOffset.setY(0); } p.drawImage(shadowOffset, shadowImage); QPoint originalOffset(0, 0); if (canResize) { if (offset.x() < 0) originalOffset.setX(qAbs(offset.x())); if (offset.y() < 0) originalOffset.setY(qAbs(offset.y())); } p.drawPixmap(originalOffset, QPixmap::fromImage(image)); p.end(); return shadowed; 

Here we first create a shadow image using artful drawing with different composition modes, and then draw it and the original image over. With the necessary shifts, of course.

Colorization


To achieve the effect of colorization, there are many different methods. I chose one, in my opinion, the most successful.

 QImage resultImage(image.size(), QImage::Format_ARGB32_Premultiplied); resultImage.fill(Qt::transparent); QPainter painter(&resultImage); painter.drawImage(0, 0, grayscaled(image)); painter.setCompositionMode(QPainter::CompositionMode_Screen); painter.fillRect(resultImage.rect(), color); painter.end(); resultImage.setAlphaChannel(image.alphaChannel()); return resultImage; 

Here we simply draw the original image in shades of gray (we already know how), and then overlay the rectangle of the desired color in the Composition mode Screen. And do not forget about the alpha channel.

Change transparency


Now we will make our image transparent. It's very simple - done with QPainter::setOpacity .

 QImage resultImage(image.size(), QImage::Format_ARGB32); resultImage.fill(Qt::transparent); QPainter painter(&resultImage); painter.setOpacity(opacity); painter.drawImage(0, 0, image); painter.end(); resultImage.setAlphaChannel(image.alphaChannel()); return resultImage; 


Rotate the picture


Rotate around the center. The implementation of the rotation around an arbitrary point will leave readers as homework. Everything is also very simple here - the main thing is not to forget about smooth transformations.

 QImage rotated(image.size(), QImage::Format_ARGB32_Premultiplied); rotated.fill(Qt::transparent); QPainter p(&rotated); p.setRenderHint(QPainter::Antialiasing); p.setRenderHint(QPainter::SmoothPixmapTransform); qreal dx = image.size().width() / 2.0, dy = image.size().height() / 2.0; p.translate(dx, dy); p.rotate(angle); p.translate(-dx, -dy); p.drawImage(0, 0, image); p.end(); return rotated; 

Grand final


Everything, now you can write a test program (or download mine from GitHub 'a) and enjoy the results!

As a bonus I will give a small function for more convenient reading of color from a string value. Qt for some reason does not understand the color in the #RRGGBBAA format, which I replenished with my function:
 QColor ImageManager::resolveColor(const QString & name) { QColor color; if (QColor::isValidColor(name)) color.setNamedColor(name); else { // trying to parse "#RRGGBBAA" color if (name.length() == 9) { QString solidColor = name.left(7); if (QColor::isValidColor(solidColor)) { color.setNamedColor(solidColor); int alpha = name.right(2).toInt(0, 16); color.setAlpha(alpha); } } } if (!color.isValid()) qWarning() << QString("[ImageManager::resolveColor] Can\'t parse color: %1").arg(name); return color; } 

At the same time, all standard colors (such as white , transparent , #ffa0ee ) are also well understood.

PS: For those who doubt whether it is worth investigating the example code on a githaba, I’ll leave a couple of lines here. Firstly, the code in the article is a bit simplified - some useful checks and so on have been removed. Secondly, the full example uses the retrieval and preservation / use of cookies when requested. Thirdly, there are additional functions for drawing a picture consisting of nine parts (nine-part image), which can simplify the manual drawing of buttons, input fields and other similar things. So the buns are provided!

PPS: If someone knows more successful algorithms to perform all the considered tasks, welcome to express them in the comments! The same applies to other image processing methods - I’m happy to read about them.

Source: https://habr.com/ru/post/139265/


All Articles