Pull to refresh

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

Reading time10 min
Views11K
Доброго всем хабрадня!

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


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


Для загрузки изображений мы будем использовать 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);
};

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



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

Далее пойдут достаточно интересные.

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


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

Первый способ заключается в конвертировании изображения в формат 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;

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

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


Тут алгоритм достаточно интересен. Первая мысль моя была — создать маску и обрезать по ней изображение. Но после долгих безуспешных попыток правильно эту самую маску нарисовать с помощью 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. Простенько и со вкусом, как говорится.

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


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

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;

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

Колоризация


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

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. И не забываем про альфа-канал.

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


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


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


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

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 высказывать их в комментариях! То же касается и иных методов обработки изображений — с удовольствием о них почитаю.
Tags:
Hubs:
+44
Comments24

Articles

Change theme settings