Доброго всем хабрадня!
Сегодня я хочу рассказать об одном из методов синхронной и асинхронной загрузки изображения из сети. Чтобы статья была не скучной, загруженные изображения мы попробуем каким-либо образом обработать средствами Qt.
Для загрузки изображений мы будем использовать
Есть замечательный класс QGraphicsEffect с подклассами. Но мы с ними работать в рамках данной статьи не будем, смиритесь! И я даже объясню почему. К примеру, в Qt 4.8.0 эти эффекты ведут к крашу приложения в Mac OS X 10.7.+, а в Qt 4.7.4 в той же системе они вообще не работают. Уж не знаю, как так вышло, но багу в багтрекере Qt я поставил.
Значит, будем создавать свой класс для обработки изображений. Он будет у нас уметь слудющее:
Сразу отмечу, что полный код тестового проекта можно скачать на гитхабе, ссылка в конце статьи.
Для начала определимся, чего мы хотим. А хотим мы вот чего: вызываем некий метод некоего класса, передаём в него URL картинки, а так же какому объекту передать полученное изображение и в какой метод. И когда изображение будет загружено, наш класс должен вызвать нужный метод нужного объекта и передать в него скачанную картинку. И всё это асинхронно. Звучит неплохо?
За дело! Создаём класс
Собственно, наш класс будет уметь грузить картинки как синхронно, так и асинхронно. Так что выбор есть.
Пример использования:
Немного пояснений по поводу страхолюдного приватного класса.этот ужас
Теперь самое интересное — имплементация функций приватного класса (класс
Разберём теперь эти функции. В конструкторе мы создаём
Для синхронной загрузки, мы выполняем запрос и запускаем Event Loop, который будет завершён по окончанию загрузки. При этом мы ещё проверяем редирект и его зацикленность, дабы пользователь мог вводить любые ссылки на картинки, в том числе, пропущенные через сокращалки ссылок.
Ну а когда мы получили нашу картинку, запускаем
При асинхронной загрузке всё хитрее. Мы сохраняем запрос (точнее, ссылку на ответ), целевой объект и его метод в наш страшный
Что ж, можно делать простую форму, в которую можно вбить URL, нажать на кнопочку и получить изображение из сети. Синхронно или асинхронно. И радоваться.
Но мы пойдём дальше, и полученное изображение будем менять.
Создаём ещё один класс (у меня он опять статический, хотя уже совсем без причин на то), назовём его
Он позволит нам получить примерно такую картинку (см. тестовый проект в конце статьи):
Пойдём по порядку. Первый метод наименее интересен, он всего лишь нормализует размер изображение по максимальной стороне. Третий метод тоже не особо интересен — он делает изображение квадратным (подгоняя размер и обрезая лишнее). Их исходники я даже не буду включать в статью.
Далее пойдут достаточно интересные.
Я нашёл даже два способа это сделать, но протестировать оба на скорость пока не удосужился. Так что привожу два на выбор.
Первый способ заключается в конвертировании изображения в формат QImage::Format_Indexed8, что означает перевод изображение в индексируемый 8-битный цвет. Для этого надо создать «карту цветов» из 256 элементов — от белого до чёрного.
Второй же метод основан на прямой работе с битами изображения. Проходимся по всем пикселям и выставляем им значение серого цвета.
Второй метод, на мой взгляд, должен работать быстрее, так как не создаётся дополнительного изображения. Кроме того, он подходит так же и для изображений с прозрачностью, что тоже очень даже хорошо. Именно поэтому используется в финале именно он.
Тут алгоритм достаточно интересен. Первая мысль моя была — создать маску и обрезать по ней изображение. Но после долгих безуспешных попыток правильно эту самую маску нарисовать с помощью QPainter::draw[Ellipse|Arc|RoundedRect|Path], я отказался от этой идеи. Почему-то такой подход даёт хороший результат лишь для некоторых радиусов скругления. Кроме того, результат может быть разным в разных ОС, что тоже не делает чести данной методе. Это, видимо, происходит из-за невозможности сделать антиалиасинг для битовой маски — у неё должно быть лишь два цвета, чёрный и белый. Новый метод смеётся над этими проблемами, и даёт дополнительную плюшку в виде гладкого скругления с антиалиасингом.
Суть почти такая же, как и маскирование картинки. Создаём скруглённый чёрный квадрат (с антиалиасингом), а затем рисуем поверх него исходное изображение с режимом композиции
Теперь попробуем добавить тень к изображению. Причём, с учётом прозрачности. Получившаяся картинка, разумеется, может иметь размеры, отличные от исходных.
Здесь мы сначала создаём изображение тени с помощью хитрого рисования с разными режимами композиции, а затем рисуем его и исходное изображение поверх. С необходимыми сдвигами, разумеется.
Для достижения эффекта колоризации существует множество различных методов. Я выбрал один, на мой взгляд, самый удачный.
Здесь мы просто рисуем исходную картинку в оттенках серого (благо уже знаем как), а затем накладываем поверх прямоугольник нужного цвета в режиме композиции Screen. И не забываем про альфа-канал.
Теперь сделаем нашу картинку прозрачной. Это совсем просто — делается с помощью
Вращать будем вокруг центра. Реализацию вращения вокруг произвольной точки оставлю читателям как домашнее задание. Тут всё тоже предельно просто — главное не забыть про гладкие преобразования.
Всё, теперь можно писать тестовую программу (или скачать мою с GitHub'а) и радоваться полученным результатам!
В качестве бонуса приведу небольшую функцию для более удобного чтения цвета из строкового значения. Qt почему-то не понимает цвет в формате
При этом, все стандартные цвета (типа
PS: Для тех, кто сомневается — стоит ли исследовать код примера на гитхабе, оставлю тут пару строк. Во-первых, код в статье немного упрощён — убраны некоторые полезные проверки и прочее. Во-вторых, в полном примере используется получение и сохранение/использование кукисов при запросе. В-третьих, там имеются дополнительные функции для рисования картинки, состоящей из девяти частей (nine-part image), что может упростить ручную отрисовку кнопок, полей ввода и прочих подобных вещей. Так что плюшки обеспечены!
PPS: Если кто-то знает более удачные алгоритмы для выполнения всех рассмотренных задач, welcome высказывать их в комментариях! То же касается и иных методов обработки изображений — с удовольствием о них почитаю.
Сегодня я хочу рассказать об одном из методов синхронной и асинхронной загрузки изображения из сети. Чтобы статья была не скучной, загруженные изображения мы попробуем каким-либо образом обработать средствами 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 высказывать их в комментариях! То же касается и иных методов обработки изображений — с удовольствием о них почитаю.