From 0148f7aaa5fa2300b6e5887fa9ba7dbfd6405fae Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 18 Dec 2018 18:31:51 +0100 Subject: [PATCH] Can load remote images and cache them locally. --- ComunicMessages.pro | 9 +- config.h | 5 + data/qlabelholder.h | 41 +++++++ helpers/imageloadhelper.cpp | 163 ++++++++++++++++++++++++++ helpers/imageloadhelper.h | 91 ++++++++++++++ res/baseline_person_black_48dp.png | Bin 0 -> 544 bytes res/ressources.qrc | 1 + utils/filesutils.cpp | 19 +++ utils/filesutils.h | 28 +++++ widgets/conversationmessagewidget.cpp | 2 + widgets/conversationmessagewidget.ui | 23 +++- 11 files changed, 379 insertions(+), 3 deletions(-) create mode 100644 data/qlabelholder.h create mode 100644 helpers/imageloadhelper.cpp create mode 100644 helpers/imageloadhelper.h create mode 100755 res/baseline_person_black_48dp.png create mode 100644 utils/filesutils.cpp create mode 100644 utils/filesutils.h diff --git a/ComunicMessages.pro b/ComunicMessages.pro index 9175599..60e4a1f 100644 --- a/ComunicMessages.pro +++ b/ComunicMessages.pro @@ -31,7 +31,9 @@ SOURCES += \ helpers/conversationhelper.cpp \ data/conversationmessage.cpp \ widgets/conversationmessagewidget.cpp \ - data/conversationmessageslist.cpp + data/conversationmessageslist.cpp \ + helpers/imageloadhelper.cpp \ + utils/filesutils.cpp HEADERS += \ helpers/accounthelper.h \ @@ -64,7 +66,10 @@ HEADERS += \ helpers/conversationhelper.h \ data/conversationmessage.h \ widgets/conversationmessagewidget.h \ - data/conversationmessageslist.h + data/conversationmessageslist.h \ + helpers/imageloadhelper.h \ + utils/filesutils.h \ + data/qlabelholder.h FORMS += \ widgets/loginwidget.ui \ diff --git a/config.h b/config.h index 411ab13..4944658 100644 --- a/config.h +++ b/config.h @@ -35,4 +35,9 @@ #define CONVERSATION_MESSAGE_MIN_LENGTH 3 #define CONVERSATION_MESSAGES_REFRESH_INTERVAL 1000 +/** + * Images load manager information + */ +#define REMOTE_IMAGES_CACHE_DIRECTORY "remote_images" + #endif // CONFIG_H diff --git a/data/qlabelholder.h b/data/qlabelholder.h new file mode 100644 index 0000000..e504e27 --- /dev/null +++ b/data/qlabelholder.h @@ -0,0 +1,41 @@ +/** + * QLabelHolder + * + * This file should be referenced by ImageLoadHelper ONLY !!! + * + * @author Pierre HUBERT + */ + +#pragma once + +#include +#include + +/** + * This class is used to avoid memory leak if attempting to + * apply downloaded image to a deleted label + */ +class QLabelHolder : public QObject +{ + Q_OBJECT +public: + QLabelHolder(QLabel *label) { + mLabel = label; + connect(label, &QLabel::destroyed, this, &QLabelHolder::deleted); + } + + QLabel *label(){ + return mLabel; + } + +private slots: + + void deleted(){ + mLabel = nullptr; + } + +private: + + //Private fields + QLabel *mLabel; +}; diff --git a/helpers/imageloadhelper.cpp b/helpers/imageloadhelper.cpp new file mode 100644 index 0000000..d3b9ba1 --- /dev/null +++ b/helpers/imageloadhelper.cpp @@ -0,0 +1,163 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "imageloadhelper.h" +#include "../config.h" +#include "../utils/filesutils.h" + + +//This header should be called only from this file +// (QObject subclass seems not to be declarable in CPP files) +#include "../data/qlabelholder.h" + +/** + * The list of pending labels for an image + */ +static QMap> privateList; + +/** + * Network Access Manager used to download images + */ +static QNetworkAccessManager accessManager; + +/** + * The first and the last instance of this object + */ +static ImageLoadHelper *mHelper = nullptr; + +ImageLoadHelper::ImageLoadHelper(QObject *parent) : QObject(parent) +{ + //Nothing yet +} + +void ImageLoadHelper::Load(QLabel *label, const QString &url) +{ + //Construct object if required + if(mHelper == nullptr) + mHelper = new ImageLoadHelper(); + + //Check if the image has already been downloaded + if(IsDownloaded(url)){ + ApplyImage(label, url); + return; + } + + //Check if download is already running + if(privateList.contains(url)){ + QList swapList = privateList.take(url); + swapList.append(new QLabelHolder(label)); + privateList.insert(url, swapList); + return; + } + + //If we get there, we have to launch the download of the image + QList list; + list.append(new QLabelHolder(label)); + privateList.insert(url, list); + + Download(url); +} + +void ImageLoadHelper::NetworkError() +{ + qWarning("Image Load Helper error : Network error while connecting to server!"); +} + +void ImageLoadHelper::NetworkRequestFinished() +{ + //If the request is a success, we can save the image, else we can not do anything + QNetworkReply *reply = qobject_cast(sender()); + reply->deleteLater(); + + if(reply->error() != QNetworkReply::NoError){ + qWarning("Can not process image because we encountered an error while downloading it!"); + return; + } + + //Save the image + QString url = reply->url().toString(); + QImageReader reader(reply); + QImage image = reader.read(); + + //Check if the image could not be decode + if(image.isNull()){ + qWarning("Downloaded image from %s is not valid!", url.toStdString().c_str()); + return; + } + + //Save the image + QString file_path = GetImageStoragePath(url); + if(!image.save(file_path, "PNG")){ + qWarning("Could not save image from %s to %s !", url.toStdString().c_str(), file_path.toStdString().c_str()); + return; + } + + //Process the list of awaiting labels + QPixmap pixmap = QPixmap::fromImage(image); + QList labels = privateList.take(url); + for(QLabelHolder *currLabel : labels) + if(currLabel->label() != nullptr) + ApplyImage(currLabel->label(), pixmap); +} + +void ImageLoadHelper::SSLErrors() +{ + qWarning("Image Load Helper error : SSL error while connecting to server!"); +} + +void ImageLoadHelper::Download(const QString &url) +{ + qWarning("Download image located at URL: %s", url.toStdString().c_str()); + + QNetworkRequest request; + request.setUrl(QUrl(url)); + request.setRawHeader("User-Agent", "ComunicMessages Images Downloader 1.0"); + + QNetworkReply *reply = accessManager.get(request); + connect(reply, &QNetworkReply::finished, mHelper, &ImageLoadHelper::NetworkRequestFinished); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), mHelper, SLOT(NetworkError())); + connect(reply, &QNetworkReply::sslErrors, mHelper, &ImageLoadHelper::SSLErrors); +} + +void ImageLoadHelper::ApplyImage(QLabel *label, const QString &url) +{ + ApplyImage(label, QPixmap(GetImageStoragePath(url))); +} + +void ImageLoadHelper::ApplyImage(QLabel *label, const QPixmap &pixmap) +{ + label->setPixmap(pixmap); +} + +bool ImageLoadHelper::IsDownloaded(const QString &url) +{ + return QFile(GetImageStoragePath(url)).exists(); +} + +QString ImageLoadHelper::GetImageStoragePath(const QString &url) +{ + //Containing directory + QString path = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + + QDir::separator() + REMOTE_IMAGES_CACHE_DIRECTORY; + + //Check the directory exists. If not, create it + if(!FilesUtils::CreateDirectoryIfNotExists(path)) + qFatal("Could not create images cache directory."); + + //Add separator to path + path += QDir::separator(); + + //File name + path += QString(QCryptographicHash::hash(url.toStdString().c_str(), QCryptographicHash::Md5).toHex()); + + return path; +} diff --git a/helpers/imageloadhelper.h b/helpers/imageloadhelper.h new file mode 100644 index 0000000..e7c8ac2 --- /dev/null +++ b/helpers/imageloadhelper.h @@ -0,0 +1,91 @@ +/** + * ImageLoadHelper - This helper is used to + * easily load remote images and caches them + * + * @author Pierre HUBERT + */ + +#ifndef IMAGELOADHELPER_H +#define IMAGELOADHELPER_H + +#include + +class QLabel; + +class ImageLoadHelper : public QObject +{ + Q_OBJECT +public: + + /** + * Load an image into a label + * + * @param label The label where the image will be applied + * @param url The URL of the target image + */ + static void Load(QLabel *label, const QString &url); + +signals: + +private slots: + + /** + * Slot called in case of SSL error + */ + void SSLErrors(); + + /** + * Slot called in case of network error + */ + void NetworkError(); + + /** + * Slot called when a network request finished + */ + void NetworkRequestFinished(); + +private: + //This constructor must be private + explicit ImageLoadHelper(QObject *parent = nullptr); + + /** + * Download an image + * + * @param url The URL of the image to download + */ + static void Download(const QString &url); + + /** + * Apply an image to a label + * + * @param label Target label which will receive the image + * @param url The remote URL of the image + */ + static void ApplyImage(QLabel *label, const QString &url); + + /** + * Apply an image to a label + * + * @param label Target label which will receive the image + * @param pixamp The pixmap to apply to the label + */ + static void ApplyImage(QLabel *label, const QPixmap &pixmap); + + /** + * Check out whether an image has been already downloaded + * + * @param url The URL of the target image + * @return TRUE for a success / FALSE else + */ + static bool IsDownloaded(const QString &url); + + /** + * Get the storage path of a remote image + * + * @param url The URL of the remote image + * @return Full path to the image + */ + static QString GetImageStoragePath(const QString &url); +}; + +#endif // IMAGELOADHELPER_H diff --git a/res/baseline_person_black_48dp.png b/res/baseline_person_black_48dp.png new file mode 100755 index 0000000000000000000000000000000000000000..f29d942ced8de2b1fd0c36c5a47ce4e34a26b9bb GIT binary patch literal 544 zcmV+*0^j|KP)qV6u@&|TCWK^!__ zzfl{=VtlUesRXP?0=5A!v<03{x7PQ9W-;DKav7nG8_Fv=i zXYO!-T`VF?FEPB+$p~AxNExZ15*OLR2%V^lGr%&gA_2R~G6smDqL0;lKydFJtLdYL zl4LoHgxp!OB#7KJhIxZT)N6*BMuhsgfkfO5`iVq}tw@d9N{WzmauumzSLq}~-8@2S z+(UW@NrrofLifn%p8&YE7TnE46q}E;m55?1adrh!>@v;@h++ktiE?%Ub^&$)b^%dj i7hsGr#u#IaF<$`O8Y;kAO%NRb0000 baseline_people_black_48dp.png baseline_access_time_black_48dp.png + baseline_person_black_48dp.png diff --git a/utils/filesutils.cpp b/utils/filesutils.cpp new file mode 100644 index 0000000..2cb3a61 --- /dev/null +++ b/utils/filesutils.cpp @@ -0,0 +1,19 @@ +#include + +#include "filesutils.h" + +FilesUtils::FilesUtils() +{ + +} + +bool FilesUtils::CreateDirectoryIfNotExists(const QString &path) +{ + QDir dir(path); + + //Check if the directory already exists + if(dir.exists()) + return true; + + return dir.mkpath("."); +} diff --git a/utils/filesutils.h b/utils/filesutils.h new file mode 100644 index 0000000..307bab0 --- /dev/null +++ b/utils/filesutils.h @@ -0,0 +1,28 @@ +/** + * Files utilities + * + * @author Pierre HUBERT + */ + +#ifndef FILESUTILS_H +#define FILESUTILS_H + +#include + +class FilesUtils +{ +public: + FilesUtils(); + + /** + * Create the specified directory and all its parents if they do not + * exists + * + * @param path The path of the folder to check + * @return TRUE if the folder exists or has been successfully created / + * FALSE else + */ + static bool CreateDirectoryIfNotExists(const QString &path); +}; + +#endif // FILESUTILS_H diff --git a/widgets/conversationmessagewidget.cpp b/widgets/conversationmessagewidget.cpp index 505899e..90066c6 100644 --- a/widgets/conversationmessagewidget.cpp +++ b/widgets/conversationmessagewidget.cpp @@ -2,6 +2,7 @@ #include "ui_conversationmessagewidget.h" #include "../data/user.h" #include "../data/conversationmessage.h" +#include "../helpers/imageloadhelper.h" ConversationMessageWidget::ConversationMessageWidget(QWidget *parent) : QWidget(parent), @@ -19,4 +20,5 @@ void ConversationMessageWidget::setMessage(const ConversationMessage &message, c { ui->nameLabel->setText(user.displayName()); ui->messageLabel->setText(message.message()); + ImageLoadHelper::Load(ui->accountImageLabel, user.accountImage()); } diff --git a/widgets/conversationmessagewidget.ui b/widgets/conversationmessagewidget.ui index aacd7e6..d517891 100644 --- a/widgets/conversationmessagewidget.ui +++ b/widgets/conversationmessagewidget.ui @@ -14,6 +14,25 @@ Form + + + + + 20 + 20 + + + + + + + :/baseline_person_black_48dp.png + + + true + + + @@ -54,6 +73,8 @@ - + + +