Qt Software / Синхронная и асинхронная загрузка изображения из сети с последующей обработкой

в 14:49, , рубрики: async, c++, effects, GUI, http, images, network, QImage, qt, shadow, sync, метки: , , , , , , , , , ,

Доброго всем хабрадня!

Сегодня я хочу рассказать об одном из методов синхронной и асинхронной загрузки изображения из сети. Чтобы статья была не скучной, загруженные изображения мы попробуем каким-либо образом обработать средствами Qt.
Qt Software / Синхронная и асинхронная загрузка изображения из сети с последующей обработкой

Как будем загружать?

Для загрузки изображений мы будем использовать QNetworkAccessManager и QEventLoop, а так же немного мета-объектов. Загружать будем по HTTP изображение в любом формате, из поддерживаемых Qt. Ну, ещё будем обрабатывать редиректы.

Как обрабатывать-то будем?

Есть замечательный класс QGraphicsEffect с подклассами. Но мы с ними работать в рамках данной статьи не будем, смиритесь! И я даже объясню почему. К примеру, в Qt 4.8.0 эти эффекты ведут к крашу приложения в Mac OS X 10.7.+, а в Qt 4.7.4 в той же системе они вообще не работают. Уж не знаю, как так вышло, но багу в багтрекере Qt я поставил.

Значит, будем создавать свой класс для обработки изображений. Он будет у нас уметь слудющее:

  • Переводить изображение в оттенки серого
  • Колоризировать (как это по русски-то сказать?)
  • Добавлять тень
  • Менять прозрачность
  • Вращать вокруг центра
  • Квадратизировать
  • Квадратизировать со скруглением углов
  • Как бонус, научимся считывать пользовательские цвета в формате #RRGGBBAA

Сразу отмечу, что полный код тестового проекта можно скачать на гитхабе, ссылка в конце статьи.

Итак, загрузка изображения

Для начала определимся, чего мы хотим. А хотим мы вот чего: вызываем некий метод некоего класса, передаём в него URL картинки, а так же какому объекту передать полученное изображение и в какой метод. И когда изображение будет загружено, наш класс должен вызвать нужный метод нужного объекта и передать в него скачанную картинку. И всё это асинхронно. Звучит неплохо?

За дело! Создаём класс Networking (я его сделал статическим, но это не играет большой роли), и создаём класс NetworkingPrivate — для настоящей работы.

// 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; }; 

Собственно, наш класс будет уметь грузить картинки как синхронно, так и асинхронно. Так что выбор есть.

Пример использования:

// 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"); } 

Немного пояснений по поводу страхолюдного приватного класса. QNetworkAccessManager нам нужен для отправки http-запросов, QEventLoop — для ожидания ответа в случае синхронных запросов, а этот ужас QMap<QNetworkReply*, QPair<QObject*, QPair<QUrl, const char *> > > requests — для хранения всех запросов, чтобы знать, какая картинка к какому объекту должна быть доставлена после загрузки.

Теперь самое интересное — имплементация функций приватного класса (класс Networking, как Вы уже догадались, лишь переадресует вызовы своему приватному классу).

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(); 	} } 

Разберём теперь эти функции. В конструкторе мы создаём QEventLoop, QNetworkAccessManager и соединяем сигнал о завершении запроса с QEventLoop::quit() и нашим методом onFinished.

Для синхронной загрузки, мы выполняем запрос и запускаем Event Loop, который будет завершён по окончанию загрузки. При этом мы ещё проверяем редирект и его зацикленность, дабы пользователь мог вводить любые ссылки на картинки, в том числе, пропущенные через сокращалки ссылок.

Ну а когда мы получили нашу картинку, запускаем QImageReader и читаем данные в финальный QImage, который и возвращаем.

При асинхронной загрузке всё хитрее. Мы сохраняем запрос (точнее, ссылку на ответ), целевой объект и его метод в наш страшный QMap, после чего запускаем запрос. А по окончании запроса делаем всё то же самое, что и при синхронном запросе (проверка на редирект, его цикличность и чтение картинки), но полученный QImage передаём целевому объекту с помощью QMetaObject::invokeMethod. В качестве параметров — URL запроса и картинка.

Что ж, можно делать простую форму, в которую можно вбить URL, нажать на кнопочку и получить изображение из сети. Синхронно или асинхронно. И радоваться.

Но мы пойдём дальше, и полученное изображение будем менять.

Класс для обработки изображений

Создаём ещё один класс (у меня он опять статический, хотя уже совсем без причин на то), назовём его ImageManager. И будут у нас следующие методы в нём:

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); }; 

Он позволит нам получить примерно такую картинку (см. тестовый проект в конце статьи):

Qt Software / Синхронная и асинхронная загрузка изображения из сети с последующей обработкой

Пойдём по порядку. Первый метод наименее интересен, он всего лишь нормализует размер изображение по максимальной стороне. Третий метод тоже не особо интересен — он делает изображение квадратным (подгоняя размер и обрезая лишнее). Их исходники я даже не буду включать в статью.

Далее пойдут достаточно интересные.
Qt Software / Синхронная и асинхронная загрузка изображения из сети с последующей обработкой

Оттенки серого.

Я нашёл даже два способа это сделать, но протестировать оба на скорость пока не удосужился. Так что привожу два на выбор.

Первый способ заключается в конвертировании изображения в формат QImage::Format_Indexed8, что означает перевод изображение в индексируемый 8-битный цвет. Для этого надо создать «карту цветов» из 256 элементов — от белого до чёрного.

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; 

Второй же метод основан на прямой работе с битами изображения. Проходимся по всем пикселям и выставляем им значение серого цвета.

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; 

Второй метод, на мой взгляд, должен работать быстрее, так как не создаётся дополнительного изображения. Кроме того, он подходит так же и для изображений с прозрачностью, что тоже очень даже хорошо. Именно поэтому используется в финале именно он.
Qt Software / Синхронная и асинхронная загрузка изображения из сети с последующей обработкой

Скругление углов

Тут алгоритм достаточно интересен. Первая мысль моя была — создать маску и обрезать по ней изображение. Но после долгих безуспешных попыток правильно эту самую маску нарисовать с помощью QPainter::draw[Ellipse|Arc|RoundedRect|Path], я отказался от этой идеи. Почему-то такой подход даёт хороший результат лишь для некоторых радиусов скругления. Кроме того, результат может быть разным в разных ОС, что тоже не делает чести данной методе. Это, видимо, происходит из-за невозможности сделать антиалиасинг для битовой маски — у неё должно быть лишь два цвета, чёрный и белый. Новый метод смеётся над этими проблемами, и даёт дополнительную плюшку в виде гладкого скругления с антиалиасингом.

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; 

Суть почти такая же, как и маскирование картинки. Создаём скруглённый чёрный квадрат (с антиалиасингом), а затем рисуем поверх него исходное изображение с режимом композиции QPainter::CompositionMode_SourceIn. Простенько и со вкусом, как говорится.
Qt Software / Синхронная и асинхронная загрузка изображения из сети с последующей обработкой

Добавление тени

Теперь попробуем добавить тень к изображению. Причём, с учётом прозрачности. Получившаяся картинка, разумеется, может иметь размеры, отличные от исходных.

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; 

Здесь мы сначала создаём изображение тени с помощью хитрого рисования с разными режимами композиции, а затем рисуем его и исходное изображение поверх. С необходимыми сдвигами, разумеется.
Qt Software / Синхронная и асинхронная загрузка изображения из сети с последующей обработкой

Колоризация

Для достижения эффекта колоризации существует множество различных методов. Я выбрал один, на мой взгляд, самый удачный.

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; 

Здесь мы просто рисуем исходную картинку в оттенках серого (благо уже знаем как), а затем накладываем поверх прямоугольник нужного цвета в режиме композиции Screen. И не забываем про альфа-канал.
Qt Software / Синхронная и асинхронная загрузка изображения из сети с последующей обработкой

Изменение прозрачности

Теперь сделаем нашу картинку прозрачной. Это совсем просто — делается с помощью 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; 

Qt Software / Синхронная и асинхронная загрузка изображения из сети с последующей обработкой

Вращаем картинку

Вращать будем вокруг центра. Реализацию вращения вокруг произвольной точки оставлю читателям как домашнее задание. Тут всё тоже предельно просто — главное не забыть про гладкие преобразования.

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

Всё, теперь можно писать тестовую программу (или скачать мою с GitHub'а) и радоваться полученным результатам!

В качестве бонуса приведу небольшую функцию для более удобного чтения цвета из строкового значения. Qt почему-то не понимает цвет в формате #RRGGBBAA, что я и восполнил своей функцией:

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; } 

При этом, все стандартные цвета (типа white, transparent, #ffa0ee) так же замечательно понимаются.

PS: Для тех, кто сомневается — стоит ли исследовать код примера на гитхабе, оставлю тут пару строк. Во-первых, код в статье немного упрощён — убраны некоторые полезные проверки и прочее. Во-вторых, в полном примере используется получение и сохранение/использование кукисов при запросе. В-третьих, там имеются дополнительные функции для рисования картинки, состоящей из девяти частей (nine-part image), что может упростить ручную отрисовку кнопок, полей ввода и прочих подобных вещей. Так что плюшки обеспечены!

PPS: Если кто-то знает более удачные алгоритмы для выполнения всех рассмотренных задач, welcome высказывать их в комментариях! То же касается и иных методов обработки изображений — с удовольствием о них почитаю.

Автор: silvansky


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js