Verified Commit cad08926 authored by Fushan Wen's avatar Fushan Wen 💬
Browse files

wallpapers/image: try to extract image metadata if available

Currently title, author and resolution are extracted.
parent b3943ee1
Pipeline #201666 passed with stage
in 10 minutes and 46 seconds
......@@ -264,6 +264,16 @@ if(${ICU_FOUND})
set(HAVE_ICU TRUE)
endif()
find_package(KF5KExiv2)
set_package_properties(KF5KExiv2
PROPERTIES URL "https://www.exiv2.org" DESCRIPTION "Image metadata support"
TYPE OPTIONAL
PURPOSE "Provides metadata for image wallpaper plugin"
)
if(${KF5KExiv2_FOUND})
set(HAVE_KF5KExiv2 TRUE)
endif()
find_package(KIOExtras)
set_package_properties(KIOExtras PROPERTIES DESCRIPTION "Common KIO slaves for operations."
PURPOSE "Show thumbnails in wallpaper selection."
......
......@@ -3,10 +3,10 @@ set(image_SRCS
slidemodel.cpp
slidefiltermodel.cpp
sortingmode.h
finder/imagesizefinder.cpp
finder/distance.cpp
finder/findsymlinktarget.h
finder/imagefinder.cpp
finder/mediametadatafinder.cpp
finder/suffixcheck.cpp
finder/packagefinder.cpp
model/abstractimagelistmodel.cpp
......@@ -43,6 +43,11 @@ target_link_libraries(plasma_wallpaper_imageplugin_static
PW::LibTaskManager
)
if(HAVE_KF5KExiv2)
target_link_libraries(plasma_wallpaper_imageplugin_static KF5::KExiv2)
endif()
configure_file(config-KF5KExiv2.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-KF5KExiv2.h)
add_library(plasma_wallpaper_imageplugin SHARED imageplugin.cpp)
target_link_libraries(plasma_wallpaper_imageplugin
plasma_wallpaper_imageplugin_static
......
......@@ -3,14 +3,14 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR}/.. ${CMAKE_CURRENT_SOURCE_DIR}/.
find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS QuickTest)
# ImageSizeFinder test
ecm_add_test(test_imagesizefinder.cpp TEST_NAME testimagesizefinder
LINK_LIBRARIES Qt::Test plasma_wallpaper_imageplugin_static)
# ImageFinder test
ecm_add_test(test_imagefinder.cpp TEST_NAME testimagefinder
LINK_LIBRARIES Qt::Test plasma_wallpaper_imageplugin_static)
# MediaMetadataFinder test
ecm_add_test(test_mediametadatafinder.cpp TEST_NAME testmediametadatafinder
LINK_LIBRARIES Qt::Test plasma_wallpaper_imageplugin_static)
# PackageFinder test
ecm_add_test(test_packagefinder.cpp TEST_NAME testpackageimagefinder
LINK_LIBRARIES Qt::Test plasma_wallpaper_imageplugin_static)
......
......@@ -8,8 +8,10 @@
#include <KIO/PreviewJob>
#include "../finder/mediametadatafinder.h"
#include "../model/imagelistmodel.h"
#include "commontestdata.h"
#include "config-KF5KExiv2.h"
class ImageListModelTest : public QObject
{
......@@ -42,6 +44,8 @@ private:
void ImageListModelTest::initTestCase()
{
qRegisterMetaType<MediaMetadata>();
m_dataDir = QDir(QFINDTESTDATA("testdata/default"));
m_alternateDir = QDir(QFINDTESTDATA("testdata/alternate"));
QVERIFY(!m_dataDir.isEmpty());
......@@ -92,6 +96,17 @@ void ImageListModelTest::testImageListModelData()
// Should return the complete base name for wallpaper.jpg.jpg
QCOMPARE(idx.data(Qt::DisplayRole).toString(), QStringLiteral("wallpaper.jpg"));
m_dataSpy->wait();
#if HAVE_KF5KExiv2
m_dataSpy->wait();
m_dataSpy->wait();
QCOMPARE(m_dataSpy->size(), 3);
QCOMPARE(m_dataSpy->takeFirst().at(2).value<QVector<int>>().at(0), Qt::DisplayRole);
QCOMPARE(idx.data(Qt::DisplayRole).toString(), QStringLiteral("DocumentName"));
QCOMPARE(m_dataSpy->takeFirst().at(2).value<QVector<int>>().at(0), ImageRoles::AuthorRole);
#endif
QCOMPARE(m_dataSpy->size(), 1);
QCOMPARE(m_dataSpy->takeFirst().at(2).value<QVector<int>>().at(0), ImageRoles::ResolutionRole);
QCOMPARE(idx.data(ImageRoles::ScreenshotRole), QVariant()); // Not cached yet
if (!KIO::PreviewJob::availablePlugins().empty()) {
......@@ -101,12 +116,12 @@ void ImageListModelTest::testImageListModelData()
QVERIFY(!idx.data(ImageRoles::ScreenshotRole).value<QPixmap>().isNull());
}
#if HAVE_KF5KExiv2
QCOMPARE(idx.data(ImageRoles::AuthorRole).toString(), QStringLiteral("KDE Community"));
#else
QCOMPARE(idx.data(ImageRoles::AuthorRole).toString(), QString());
#endif
QCOMPARE(idx.data(ImageRoles::ResolutionRole).toString(), QString());
m_dataSpy->wait();
QCOMPARE(m_dataSpy->size(), 1);
QCOMPARE(m_dataSpy->takeFirst().at(2).value<QVector<int>>().at(0), ImageRoles::ResolutionRole);
QCOMPARE(idx.data(ImageRoles::ResolutionRole).toString(), QStringLiteral("15x16"));
QCOMPARE(idx.data(ImageRoles::PathRole).toUrl(), QUrl::fromLocalFile(m_wallpaperPaths.at(0)));
......@@ -228,6 +243,12 @@ void ImageListModelTest::testImageListModelRemoveLocalBackground()
QPersistentModelIndex idx = m_model->index(0, 0);
QCOMPARE(idx.data(Qt::DisplayRole).toString(), QStringLiteral("wallpaper.jpg"));
m_dataSpy->wait();
#if HAVE_KF5KExiv2
m_dataSpy->wait();
m_dataSpy->wait();
#endif
m_dataSpy->clear();
QCOMPARE(idx.data(ImageRoles::RemovableRole).toBool(), true);
m_model->removeBackground(standardPath + QStringLiteral("wallpaper.jpg.jpg"));
......
......@@ -7,46 +7,50 @@
#include <QDir>
#include <QtTest>
#include "../finder/mediametadatafinder.h"
#include "commontestdata.h"
#include "finder/imagesizefinder.h"
#include "config-KF5KExiv2.h"
class ImageSizeFinderTest : public QObject
class MediaMetadataFinderTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase();
void testImageSizeFinder();
void testMediaMetadataFinderCanFindMetadata();
private:
QDir m_dataDir;
};
void ImageSizeFinderTest::initTestCase()
void MediaMetadataFinderTest::initTestCase()
{
qRegisterMetaType<MediaMetadata>();
m_dataDir = QDir(QFINDTESTDATA("testdata/default"));
QVERIFY(!m_dataDir.isEmpty());
}
void ImageSizeFinderTest::testImageSizeFinder()
void MediaMetadataFinderTest::testMediaMetadataFinderCanFindMetadata()
{
const QString path = m_dataDir.absoluteFilePath(ImageBackendTestData::defaultImageFileName1);
ImageSizeFinder *finder = new ImageSizeFinder(path);
QSignalSpy spy(finder, &ImageSizeFinder::sizeFound);
MediaMetadataFinder *finder = new MediaMetadataFinder(m_dataDir.absoluteFilePath(ImageBackendTestData::defaultImageFileName1));
QSignalSpy spy(finder, &MediaMetadataFinder::metadataFound);
QThreadPool::globalInstance()->start(finder);
spy.wait(10 * 1000);
QCOMPARE(spy.size(), 1);
QCOMPARE(spy.count(), 1);
const auto firstSignalResult = spy.takeFirst();
QCOMPARE(firstSignalResult.size(), 2);
const auto args = spy.takeFirst();
QCOMPARE(args.at(0).toString(), m_dataDir.absoluteFilePath(ImageBackendTestData::defaultImageFileName1));
QCOMPARE(firstSignalResult.at(0).toString(), path);
QCOMPARE(firstSignalResult.at(1).toSize(), QSize(15, 16));
const auto metadata = args.at(1).value<MediaMetadata>();
#if HAVE_KF5KExiv2
QTRY_COMPARE(metadata.title, QStringLiteral("DocumentName"));
QTRY_COMPARE(metadata.author, QStringLiteral("KDE Community"));
#endif
QTRY_COMPARE(metadata.resolution, QSize(15, 16));
}
QTEST_MAIN(ImageSizeFinderTest)
QTEST_MAIN(MediaMetadataFinderTest)
#include "test_imagesizefinder.moc"
#include "test_mediametadatafinder.moc"
......@@ -9,6 +9,7 @@
#include <KIO/CopyJob>
#include <KIO/PreviewJob>
#include "../finder/mediametadatafinder.h"
#include "../model/packagelistmodel.h"
#include "commontestdata.h"
......@@ -44,6 +45,8 @@ private:
void PackageListModelTest::initTestCase()
{
qRegisterMetaType<MediaMetadata>();
m_dataDir = QDir(QFINDTESTDATA("testdata/default"));
m_alternateDir = QDir(QFINDTESTDATA("testdata/alternate"));
QVERIFY(!m_dataDir.isEmpty());
......
/*
SPDX-FileCopyrightText: 2007 Paolo Capriotti <p.capriotti@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "imagesizefinder.h"
#include <QImageReader>
ImageSizeFinder::ImageSizeFinder(const QString &path, QObject *parent)
: QObject(parent)
, m_path(path)
{
}
void ImageSizeFinder::run()
{
const QImageReader reader(m_path);
Q_EMIT sizeFound(m_path, reader.size());
}
/*
SPDX-FileCopyrightText: 2007 Paolo Capriotti <p.capriotti@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef IMAGESIZEFINDER_H
#define IMAGESIZEFINDER_H
#include <QObject>
#include <QRunnable>
/**
* A runnable that helps find the dimension of an image.
*/
class ImageSizeFinder : public QObject, public QRunnable
{
Q_OBJECT
public:
explicit ImageSizeFinder(const QString &path, QObject *parent = nullptr);
void run() override;
Q_SIGNALS:
void sizeFound(const QString &path, const QSize &size);
private:
QString m_path;
};
#endif // IMAGESIZEFINDER_H
/*
SPDX-FileCopyrightText: 2007 Aurélien Gâteau <agateau@kde.org>
SPDX-FileCopyrightText: 2022 Fushan Wen <qydwhotmail@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "mediametadatafinder.h"
#include <QFile>
#include <QImageReader>
#include "config-KF5KExiv2.h"
#if HAVE_KF5KExiv2
#include <KExiv2/KExiv2>
#endif
MediaMetadataFinder::MediaMetadataFinder(const QString &path, QObject *parent)
: QObject(parent)
, m_path(path)
{
}
void MediaMetadataFinder::run()
{
MediaMetadata metadata;
const QImageReader reader(m_path);
metadata.resolution = reader.size();
#if HAVE_KF5KExiv2
KExiv2Iface::KExiv2 exivImage(m_path);
// Extract title from XPTitle
metadata.title = QString::fromUtf8(exivImage.getExifTagData("Exif.Image.XPTitle"));
// Use documentName as title
if (metadata.title.isEmpty()) {
metadata.title = QString::fromUtf8(exivImage.getExifTagData("Exif.Image.DocumentName"));
}
// Use description as title
if (metadata.title.isEmpty()) {
metadata.title = QString::fromUtf8(exivImage.getExifTagData("Exif.Image.ImageDescription"));
}
// Extract author from artist
metadata.author = QString::fromUtf8(exivImage.getExifTagData("Exif.Image.Artist"));
// Extract author from XPAuthor
if (metadata.author.isEmpty()) {
metadata.author = QString::fromUtf8(exivImage.getExifTagData("Exif.Image.XPAuthor"));
}
// Extract author from copyright
if (metadata.author.isEmpty()) {
metadata.author = QString::fromUtf8(exivImage.getExifTagData("Exif.Image.Copyright"));
}
#endif
Q_EMIT metadataFound(m_path, metadata);
}
/*
SPDX-FileCopyrightText: 2022 Fushan Wen <qydwhotmail@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef MEDIAMETADATAFINDER_H
#define MEDIAMETADATAFINDER_H
#include <QObject>
#include <QRunnable>
#include <QSize>
struct MediaMetadata {
QString title;
QString author;
QSize resolution;
};
Q_DECLARE_METATYPE(MediaMetadata)
/**
* A runnable that helps find the metadata of an image or a video.
*/
class MediaMetadataFinder : public QObject, public QRunnable
{
Q_OBJECT
public:
explicit MediaMetadataFinder(const QString &path, QObject *parent = nullptr);
void run() override;
Q_SIGNALS:
void metadataFound(const QString &path, const MediaMetadata &metadata);
private:
QString m_path;
};
#endif // MEDIAMETADATAFINDER_H
......@@ -9,6 +9,7 @@
#include <KFileItem>
#include "finder/mediametadatafinder.h"
#include "imagebackend.h"
#include "provider/packageimageprovider.h"
#include "sortingmode.h"
......@@ -29,6 +30,7 @@ void ImagePlugin::registerTypes(const char *uri)
Q_ASSERT(uri == pluginName);
qRegisterMetaType<KFileItem>(); // For image preview
qRegisterMetaType<MediaMetadata>(); // For image preview
qmlRegisterType<ImageBackend>(uri, 2, 0, "ImageBackend");
qmlRegisterType<MediaProxy>(uri, 2, 0, "MediaProxy");
......
......@@ -12,15 +12,19 @@
#include <KFileItem>
#include <KIO/PreviewJob>
#include "../finder/imagesizefinder.h"
#include "../finder/mediametadatafinder.h"
#include "config-KF5KExiv2.h"
AbstractImageListModel::AbstractImageListModel(const QSize &targetSize, QObject *parent)
: QAbstractListModel(parent)
, m_screenshotSize(targetSize / 8)
, m_targetSize(targetSize)
{
m_imageCache.setMaxCost(30);
m_imageSizeCache.setMaxCost(30);
constexpr int maxCacheSize = 30;
m_imageCache.setMaxCost(maxCacheSize);
m_backgroundTitleCache.setMaxCost(maxCacheSize);
m_backgroundAuthorCache.setMaxCost(maxCacheSize);
m_imageSizeCache.setMaxCost(maxCacheSize);
connect(this, &QAbstractListModel::rowsInserted, this, &AbstractImageListModel::countChanged);
connect(this, &QAbstractListModel::rowsRemoved, this, &AbstractImageListModel::countChanged);
......@@ -62,15 +66,6 @@ void AbstractImageListModel::slotTargetSizeChanged(const QSize &size)
reload();
}
void AbstractImageListModel::slotHandleImageSizeFound(const QString &path, const QSize &size)
{
const QPersistentModelIndex index = m_sizeJobsUrls.take(path);
if (m_imageSizeCache.insert(path, new QSize(size), 1)) {
Q_EMIT dataChanged(index, index, {ResolutionRole});
}
}
void AbstractImageListModel::slotHandlePreview(const KFileItem &item, const QPixmap &preview)
{
auto job = qobject_cast<KIO::PreviewJob *>(sender());
......@@ -164,15 +159,55 @@ void AbstractImageListModel::asyncGetPreview(const QStringList &paths, const QPe
m_previewJobsUrls.insert(index, paths);
}
void AbstractImageListModel::asyncGetImageSize(const QString &path, const QPersistentModelIndex &index) const
void AbstractImageListModel::asyncGetMediaMetadata(const QString &path, const QPersistentModelIndex &index) const
{
if (m_sizeJobsUrls.contains(path) || path.isEmpty()) {
return;
}
ImageSizeFinder *finder = new ImageSizeFinder(path);
connect(finder, &ImageSizeFinder::sizeFound, this, &AbstractImageListModel::slotHandleImageSizeFound);
MediaMetadataFinder *finder = new MediaMetadataFinder(path);
connect(finder, &MediaMetadataFinder::metadataFound, this, &AbstractImageListModel::slotMediaMetadataFound);
QThreadPool::globalInstance()->start(finder);
m_sizeJobsUrls.insert(path, index);
}
void AbstractImageListModel::clearCache()
{
m_imageCache.clear();
m_backgroundTitleCache.clear();
m_backgroundAuthorCache.clear();
m_imageSizeCache.clear();
}
void AbstractImageListModel::slotMediaMetadataFound(const QString &path, const MediaMetadata &metadata)
{
const QPersistentModelIndex index = m_sizeJobsUrls.take(path);
#if HAVE_KF5KExiv2
if (!metadata.title.isEmpty()) {
auto title = new QString(metadata.title);
if (m_backgroundTitleCache.insert(path, title, 1)) {
Q_EMIT dataChanged(index, index, {Qt::DisplayRole});
} else {
delete title;
}
}
if (!metadata.author.isEmpty()) {
auto author = new QString(metadata.author);
if (m_backgroundAuthorCache.insert(path, author, 1)) {
Q_EMIT dataChanged(index, index, {AuthorRole});
} else {
delete author;
}
}
#endif
auto resolution = new QSize(metadata.resolution);
if (m_imageSizeCache.insert(path, resolution, 1)) {
Q_EMIT dataChanged(index, index, {ResolutionRole});
} else {
delete resolution;
}
}
......@@ -16,6 +16,8 @@
class KFileItem;
struct MediaMetadata;
/**
* Base class for image list model.
*/
......@@ -60,7 +62,16 @@ protected:
* @note @c paths should have no duplicate urls.
*/
void asyncGetPreview(const QStringList &paths, const QPersistentModelIndex &index) const;
void asyncGetImageSize(const QString &path, const QPersistentModelIndex &index) const;
/**
* Asynchronously extracts metadata from an image or a video file.
*/
void asyncGetMediaMetadata(const QString &path, const QPersistentModelIndex &index) const;
/**
* Clears all cached records.
*/
void clearCache();
bool m_loading = false;
......@@ -68,6 +79,8 @@ protected:
QSize m_targetSize;
QCache<QStringList, QPixmap> m_imageCache;
QCache<QString, QString /* title */> m_backgroundTitleCache;
QCache<QString, QString /* author */> m_backgroundAuthorCache;
QCache<QString, QSize> m_imageSizeCache;
mutable QHash<QPersistentModelIndex, QStringList> m_previewJobsUrls;
......@@ -80,7 +93,7 @@ protected:
friend class ImageProxyModel; // For m_removableWallpapers
private Q_SLOTS:
void slotHandleImageSizeFound(const QString &path, const QSize &size);
void slotMediaMetadataFound(const QString &path, const MediaMetadata &metadata);
void slotHandlePreview(const KFileItem &item, const QPixmap &preview);
void slotHandlePreviewFailed(const KFileItem &item);
};
......
......@@ -37,8 +37,17 @@ QVariant ImageListModel::data(const QModelIndex &index, int role) const
const int row = index.row();
switch (role) {
case Qt::DisplayRole:
case Qt::DisplayRole: {
const QString *const title = m_backgroundTitleCache.object(m_data.at(row));
if (title) {
return title->isEmpty() ? QFileInfo(m_data.at(row)).completeBaseName() : *title;
}
asyncGetMediaMetadata(m_data.at(row), QPersistentModelIndex(index));
return QFileInfo(m_data.at(row)).completeBaseName();
}
case ScreenshotRole: {
QPixmap *cachedPreview = m_imageCache.object({m_data.at(row)});
......@@ -52,9 +61,17 @@ QVariant ImageListModel::data(const QModelIndex &index, int role) const
return QVariant();
}
case AuthorRole:
// No author for an image file?
case AuthorRole: {
const QString *const author = m_backgroundAuthorCache.object(m_data.at(row));
if (author) {
return *author;
}
asyncGetMediaMetadata(m_data.at(row), QPersistentModelIndex(index));
return QString();
}
case ResolutionRole: {
QSize *size = m_imageSizeCache.object(m_data.at(row));
......@@ -63,7 +80,7 @@ QVariant ImageListModel::data(const QModelIndex &index, int role) const
return QStringLiteral("%1x%2").arg(size->width()).arg(size->height());
}
asyncGetImageSize(m_data.at(row), QPersistentModelIndex(index));
asyncGetMediaMetadata(m_data.at(row), QPersistentModelIndex(index));
return QString();
}
......@@ -143,9 +160,7 @@ void ImageListModel::slotHandleImageFound(const QStringList &paths)
beginResetModel();
m_data = paths;
m_imageCache.clear();
m_imageSizeCache.clear();
clearCache();