Доброго всем хабрадня!
Сегодня я хочу рассказать об одном из методов синхронной и асинхронной загрузки изображения из сети. Чтобы статья была не скучной, загруженные изображения мы попробуем каким-либо образом обработать средствами 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 высказывать их в комментариях! То же касается и иных методов обработки изображений — с удовольствием о них почитаю.
Автор: silvansky