File indexing completed on 2024-05-12 15:54:49

0001 /*
0002  * SPDX-FileCopyrightText: 2021 Arjen Hiemstra <ahiemstra@heimr.nl>
0003  *
0004  * SPDX-License-Identifier: LGPL-2.0-or-later
0005  */
0006 
0007 #include "fileinfo.h"
0008 
0009 #include <optional>
0010 
0011 #include <QGlobalStatic>
0012 #include <QImageReader>
0013 #include <QMetaObject>
0014 #include <QMimeDatabase>
0015 #include <QRunnable>
0016 #include <QThreadPool>
0017 
0018 struct FileInfoCacheEntry {
0019     QUrl source;
0020     QString mimeType;
0021     FileInfo::Type type = FileInfo::UnknownType;
0022     int width = -1;
0023     int height = -1;
0024 };
0025 
0026 /**
0027  * To make FileInfo objects cheap to use from QML, we cache the information it
0028  * uses in a separate structure. This allows FileInfo to quickly lookup and
0029  * retrieve information if we have already analyzed a file before. If not, we
0030  * use a background job to retrieve the actual information so we do not block
0031  * any other thread with potentially expensive file operations.
0032  */
0033 class FileInfoCache : public QObject
0034 {
0035     Q_OBJECT
0036 public:
0037     FileInfoCache();
0038 
0039     std::shared_ptr<FileInfoCacheEntry> get(const QUrl &url);
0040 
0041     void read(const QUrl &url);
0042     void readingFinished(const QUrl &url, std::shared_ptr<FileInfoCacheEntry> entry);
0043 
0044     Q_SIGNAL void cacheUpdated(const QUrl &url);
0045 
0046     QThreadPool threadPool;
0047     QHash<QUrl, std::shared_ptr<FileInfoCacheEntry>> cache;
0048 };
0049 
0050 Q_GLOBAL_STATIC(FileInfoCache, cache);
0051 
0052 class FileInfoRunnable : public QRunnable
0053 {
0054 public:
0055     QUrl source;
0056 
0057     void run() override
0058     {
0059         auto entry = std::make_shared<FileInfoCacheEntry>();
0060         entry->source = source;
0061 
0062         QMimeDatabase db;
0063         auto mimeType = db.mimeTypeForFile(source.toLocalFile(), QMimeDatabase::MatchContent);
0064 
0065         if (!mimeType.isValid()) {
0066             // Mime type is not valid, so either the source does not exist or we
0067             // don't have permission to read it. In any case, we cannot retrieve
0068             // information for this file, so abort.
0069 
0070             // Make a local copy of the source variable so we don't need to
0071             // capture "this" which will be destroyed after it completes.
0072             auto s = source;
0073 
0074             QMetaObject::invokeMethod(
0075                 cache(),
0076                 [s]() {
0077                     cache()->readingFinished(s, nullptr);
0078                 },
0079                 Qt::QueuedConnection);
0080             return;
0081         }
0082 
0083         auto mimeTypeName = mimeType.name();
0084         entry->mimeType = mimeTypeName;
0085 
0086         if (mimeTypeName.startsWith(QStringLiteral("video/")) || //
0087             mimeTypeName == QStringLiteral("application/x-matroska")) {
0088             entry->type = FileInfo::VideoType;
0089         } else if (mimeTypeName.startsWith(QStringLiteral("image/svg"))) {
0090             entry->type = FileInfo::VectorImageType;
0091         } else if (mimeTypeName == QStringLiteral("image/gif")) {
0092             entry->type = FileInfo::AnimatedImageType;
0093         } else if (mimeTypeName.startsWith(QStringLiteral("image/"))) {
0094             entry->type = FileInfo::RasterImageType;
0095         }
0096 
0097         if (entry->type != FileInfo::VideoType) {
0098             QImageReader reader(source.toLocalFile());
0099             auto size = reader.size();
0100             if (size.isValid()) {
0101                 entry->width = size.width();
0102                 entry->height = size.height();
0103             } else {
0104                 auto image = reader.read();
0105                 entry->width = image.width();
0106                 entry->height = image.height();
0107             }
0108         }
0109 
0110         QMetaObject::invokeMethod(
0111             cache(),
0112             [entry]() {
0113                 cache()->readingFinished(entry->source, entry);
0114             },
0115             Qt::QueuedConnection);
0116     }
0117 };
0118 
0119 FileInfoCache::FileInfoCache()
0120     : QObject(nullptr)
0121 {
0122     // Since the runnable is mostly IO bound, there is not really any reason to
0123     // execute more than one of it in parallel.
0124     threadPool.setMaxThreadCount(1);
0125 }
0126 
0127 std::shared_ptr<FileInfoCacheEntry> FileInfoCache::get(const QUrl &url)
0128 {
0129     if (!url.isValid()) {
0130         return nullptr;
0131     }
0132 
0133     if (cache.contains(url)) {
0134         return cache.value(url);
0135     }
0136 
0137     return nullptr;
0138 }
0139 
0140 void FileInfoCache::read(const QUrl &url)
0141 {
0142     auto runnable = new FileInfoRunnable;
0143     runnable->source = url;
0144     threadPool.start(runnable);
0145 }
0146 
0147 void FileInfoCache::readingFinished(const QUrl &source, std::shared_ptr<FileInfoCacheEntry> entry)
0148 {
0149     if (entry) {
0150         cache.insert(source, entry);
0151     }
0152     Q_EMIT cacheUpdated(source);
0153 }
0154 
0155 FileInfo::FileInfo(QObject *parent)
0156     : QObject(parent)
0157 {
0158     connect(cache(), &FileInfoCache::cacheUpdated, this, &FileInfo::onCacheUpdated);
0159 }
0160 
0161 FileInfo::~FileInfo() = default;
0162 
0163 QUrl FileInfo::source() const
0164 {
0165     return m_source;
0166 }
0167 
0168 void FileInfo::setSource(const QUrl &source)
0169 {
0170     if (m_source == source) {
0171         return;
0172     }
0173 
0174     m_source = source;
0175     Q_EMIT sourceChanged();
0176 
0177     auto result = cache()->get(source);
0178     if (!result) {
0179         setStatus(Reading);
0180         cache()->read(source);
0181         return;
0182     }
0183 
0184     m_info = result;
0185     Q_EMIT infoChanged();
0186 
0187     setStatus(Ready);
0188 }
0189 
0190 FileInfo::Status FileInfo::status() const
0191 {
0192     return m_status;
0193 }
0194 
0195 QString FileInfo::mimeType() const
0196 {
0197     if (!m_info) {
0198         return QString{};
0199     }
0200 
0201     return m_info->mimeType;
0202 }
0203 
0204 FileInfo::Type FileInfo::type() const
0205 {
0206     if (!m_info) {
0207         return UnknownType;
0208     }
0209 
0210     return m_info->type;
0211 }
0212 
0213 int FileInfo::width() const
0214 {
0215     if (!m_info) {
0216         return -1;
0217     }
0218 
0219     return m_info->width;
0220 }
0221 
0222 int FileInfo::height() const
0223 {
0224     if (!m_info) {
0225         return -1;
0226     }
0227 
0228     return m_info->height;
0229 }
0230 
0231 void FileInfo::setStatus(FileInfo::Status newStatus)
0232 {
0233     if (newStatus == m_status) {
0234         return;
0235     }
0236 
0237     m_status = newStatus;
0238     Q_EMIT statusChanged();
0239 }
0240 
0241 void FileInfo::onCacheUpdated(const QUrl &source)
0242 {
0243     if (source != m_source) {
0244         return;
0245     }
0246 
0247     auto result = cache->get(source);
0248     if (result) {
0249         m_info = result;
0250         Q_EMIT infoChanged();
0251 
0252         setStatus(Ready);
0253     } else {
0254         setStatus(Error);
0255     }
0256 }
0257 
0258 #include "fileinfo.moc"