From 896f631591288f1297b575fafcc6cc73d722d5e5 Mon Sep 17 00:00:00 2001 From: zhaoyingzhen Date: Wed, 15 Apr 2026 11:17:13 +0800 Subject: [PATCH] feat: implement wallpaper cache plugin for dde-services The wallpapercache plugin provides the following main functions: - Wallpaper scaling: Asynchronously scales wallpapers to match different screen resolutions and caches the results for performance. - Image effects: Synchronously generates processed images (such as Gaussian blur) for the greeter and lock screen. - D-Bus API: Provides a unified interface for desktop components to request processed wallpaper paths, supporting both direct file paths and file descriptors (via FD passing). - Cache management: Automatically manages and cleans up processed image caches to optimize disk usage. Log: implement wallpaper cache plugin for dde-services Pms: TASK-384099 Change-Id: Ie67988592285bdfb9ec541ac849f5bef264852bd --- src/plugin-qt/CMakeLists.txt | 3 +- src/plugin-qt/wallpapercache/CMakeLists.txt | 67 +++ .../wallpapercache/cachedwallpaper.cpp | 247 +++++++++ .../wallpapercache/cachedwallpaper.h | 61 +++ .../wallpapercache/imageeffectprocessor.cpp | 118 +++++ .../wallpapercache/imageeffectprocessor.h | 83 +++ .../override.conf | 29 ++ .../misc/org.deepin.dde.ImageBlur1.conf | 20 + .../misc/org.deepin.dde.ImageBlur1.service | 5 + .../misc/org.deepin.dde.ImageEffect1.conf | 19 + .../misc/org.deepin.dde.ImageEffect1.service | 5 + .../misc/org.deepin.dde.WallpaperCache.conf | 29 ++ .../org.deepin.dde.WallpaperCache.service | 5 + .../misc/plugin-qt-wallpapercache.json | 19 + src/plugin-qt/wallpapercache/plugin.cpp | 84 +++ .../wallpapercache/scaleimagethread.cpp | 200 ++++++++ .../wallpapercache/scaleimagethread.h | 63 +++ .../wallpapercache/tests/CMakeLists.txt | 25 + .../wallpapercache/tests/test_dbus.sh | 481 ++++++++++++++++++ .../tests/test_wallpaper_cache.cpp | 438 ++++++++++++++++ .../wallpapercache/wallpapercache.cpp | 66 +++ src/plugin-qt/wallpapercache/wallpapercache.h | 26 + .../wallpapercache/wallpapercachemanager.cpp | 104 ++++ .../wallpapercache/wallpapercachemanager.h | 41 ++ .../wallpapercache/wallpapercacheservice.cpp | 215 ++++++++ .../wallpapercache/wallpapercacheservice.h | 102 ++++ 26 files changed, 2554 insertions(+), 1 deletion(-) create mode 100644 src/plugin-qt/wallpapercache/CMakeLists.txt create mode 100644 src/plugin-qt/wallpapercache/cachedwallpaper.cpp create mode 100644 src/plugin-qt/wallpapercache/cachedwallpaper.h create mode 100644 src/plugin-qt/wallpapercache/imageeffectprocessor.cpp create mode 100644 src/plugin-qt/wallpapercache/imageeffectprocessor.h create mode 100644 src/plugin-qt/wallpapercache/misc/deepin-service-plugin@org.deepin.dde.WallpaperCache.service.d/override.conf create mode 100644 src/plugin-qt/wallpapercache/misc/org.deepin.dde.ImageBlur1.conf create mode 100644 src/plugin-qt/wallpapercache/misc/org.deepin.dde.ImageBlur1.service create mode 100644 src/plugin-qt/wallpapercache/misc/org.deepin.dde.ImageEffect1.conf create mode 100644 src/plugin-qt/wallpapercache/misc/org.deepin.dde.ImageEffect1.service create mode 100644 src/plugin-qt/wallpapercache/misc/org.deepin.dde.WallpaperCache.conf create mode 100644 src/plugin-qt/wallpapercache/misc/org.deepin.dde.WallpaperCache.service create mode 100644 src/plugin-qt/wallpapercache/misc/plugin-qt-wallpapercache.json create mode 100644 src/plugin-qt/wallpapercache/plugin.cpp create mode 100644 src/plugin-qt/wallpapercache/scaleimagethread.cpp create mode 100644 src/plugin-qt/wallpapercache/scaleimagethread.h create mode 100644 src/plugin-qt/wallpapercache/tests/CMakeLists.txt create mode 100644 src/plugin-qt/wallpapercache/tests/test_dbus.sh create mode 100644 src/plugin-qt/wallpapercache/tests/test_wallpaper_cache.cpp create mode 100644 src/plugin-qt/wallpapercache/wallpapercache.cpp create mode 100644 src/plugin-qt/wallpapercache/wallpapercache.h create mode 100644 src/plugin-qt/wallpapercache/wallpapercachemanager.cpp create mode 100644 src/plugin-qt/wallpapercache/wallpapercachemanager.h create mode 100644 src/plugin-qt/wallpapercache/wallpapercacheservice.cpp create mode 100644 src/plugin-qt/wallpapercache/wallpapercacheservice.h diff --git a/src/plugin-qt/CMakeLists.txt b/src/plugin-qt/CMakeLists.txt index 93851d48..4d571dfd 100644 --- a/src/plugin-qt/CMakeLists.txt +++ b/src/plugin-qt/CMakeLists.txt @@ -1,7 +1,8 @@ -# SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd. +# SPDX-FileCopyrightText: 2025 - 2026 UnionTech Software Technology Co., Ltd. # # SPDX-License-Identifier: LGPL-3.0-or-later add_subdirectory("thememanager") +add_subdirectory("wallpapercache") add_subdirectory("wallpaperslideshow") add_subdirectory("xsettings") \ No newline at end of file diff --git a/src/plugin-qt/wallpapercache/CMakeLists.txt b/src/plugin-qt/wallpapercache/CMakeLists.txt new file mode 100644 index 00000000..96c5b3c1 --- /dev/null +++ b/src/plugin-qt/wallpapercache/CMakeLists.txt @@ -0,0 +1,67 @@ +# SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +cmake_minimum_required(VERSION 3.16) + +set(PLUGIN_NAME "plugin-qt-wallpapercache") + +project(${PLUGIN_NAME}) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +include(GNUInstallDirs) +file(GLOB_RECURSE SRCS "*.h" "*.cpp") + +find_package(PkgConfig REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui DBus) +pkg_check_modules(DtkCore REQUIRED dtk6core) +pkg_check_modules(DtkGui REQUIRED dtk6gui) + +add_library(${PLUGIN_NAME} MODULE + ${SRCS} +) + +target_include_directories(${PLUGIN_NAME} PRIVATE + ${DtkCore_INCLUDE_DIRS} + ${DtkGui_INCLUDE_DIRS} +) + +target_link_libraries(${PLUGIN_NAME} PRIVATE + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::DBus + ${DtkCore_LIBRARIES} + ${DtkGui_LIBRARIES} +) + +target_compile_options(${PLUGIN_NAME} PRIVATE + ${DtkCore_CFLAGS_OTHER} + ${DtkGui_CFLAGS_OTHER} +) + +set(MISC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/misc) + +install(TARGETS ${PLUGIN_NAME} DESTINATION ${CMAKE_INSTALL_LIBDIR}/deepin-service-manager/) +# Plugin descriptor for deepin-service-manager (system bus) +install(FILES ${MISC_DIR}/plugin-qt-wallpapercache.json DESTINATION share/deepin-service-manager/system/) +# D-Bus access control policy files +install(FILES + ${MISC_DIR}/org.deepin.dde.WallpaperCache.conf + ${MISC_DIR}/org.deepin.dde.ImageEffect1.conf + ${MISC_DIR}/org.deepin.dde.ImageBlur1.conf + DESTINATION share/dbus-1/system.d/ +) +# D-Bus activation service files (required for on-demand activation) +install(FILES + ${MISC_DIR}/org.deepin.dde.WallpaperCache.service + ${MISC_DIR}/org.deepin.dde.ImageEffect1.service + ${MISC_DIR}/org.deepin.dde.ImageBlur1.service + DESTINATION share/dbus-1/system-services/ +) +# systemd drop-in override (User, CacheDirectory, sandbox settings) +install(FILES + ${MISC_DIR}/deepin-service-plugin@org.deepin.dde.WallpaperCache.service.d/override.conf + DESTINATION lib/systemd/system/deepin-service-plugin@org.deepin.dde.WallpaperCache.service.d/ +) diff --git a/src/plugin-qt/wallpapercache/cachedwallpaper.cpp b/src/plugin-qt/wallpapercache/cachedwallpaper.cpp new file mode 100644 index 00000000..31251f44 --- /dev/null +++ b/src/plugin-qt/wallpapercache/cachedwallpaper.cpp @@ -0,0 +1,247 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "cachedwallpaper.h" +#include "scaleimagethread.h" +#include "imageeffectprocessor.h" + +#include +#include +#include +#include +#include +#include +#include + +CachedWallpaper::CachedWallpaper() +{ + QDir().mkpath(kBlurCacheDir); +} + +CachedWallpaper::~CachedWallpaper() +{ + m_cachedImages.clear(); +} + +CachedWallpaper *CachedWallpaper::instance() +{ + static CachedWallpaper cachedWallpaper; + return &cachedWallpaper; +} + +QStringList CachedWallpaper::getCachedImagePaths(const QString &originalPath, const QList &sizes, bool isMd5Path) +{ + QList noCachedSizes; + QStringList results; + + QString pathMd5 = ScaleImageThread::pathMd5(originalPath, isMd5Path); + + if (m_cachedImages.contains(pathMd5)) { + const QMap &map = m_cachedImages[pathMd5]; + for (const QSize &size : sizes) { + QString strSize = ScaleImageThread::sizeToString(size); + if (map.contains(strSize)) { + results.append(map[strSize]); + } else { + noCachedSizes.append(size); + } + } + } else { + noCachedSizes = sizes; + } + + if (!noCachedSizes.isEmpty()) { + qDebug() << "need handle image:" << originalPath << " sizes:" << noCachedSizes; + Q_EMIT needHandleImage(originalPath, noCachedSizes, isMd5Path); + } + + return results; +} + +void CachedWallpaper::cacheImage(const QString &originalPathMd5, const QString &size, const QString &processedPath) +{ + qDebug() << "cache Image:" << processedPath; + m_cachedImages[originalPathMd5].insert(size, processedPath); +} + +QString CachedWallpaper::getBlurImagePath(const QString &originalPath) +{ + QString pathMd5 = QCryptographicHash::hash(originalPath.toUtf8(), QCryptographicHash::Md5).toHex(); + + QMutexLocker locker(&m_blurGenerateMutex); + + auto it = m_blurImageCache.constFind(pathMd5); + if (it != m_blurImageCache.constEnd() && QFile::exists(it.value())) { + return it.value(); + } + + QString blurPath = generateBlurImage(pathMd5, originalPath); + if (!blurPath.isEmpty()) { + cacheBlurImage(pathMd5, blurPath); + return blurPath; + } + + return QString(); +} + +void CachedWallpaper::cacheBlurImage(const QString &originalPathMd5, const QString &blurPath) +{ + qDebug() << "cache blur image:" << blurPath; + m_blurImageCache[originalPathMd5] = blurPath; +} + +QStringList CachedWallpaper::getProcessedImageWithBlur(const QString &originalPath, const QList &sizes, bool needBlur) +{ + QString sourcePath = originalPath; + + if (needBlur) { + QString blurPath = getBlurImagePath(originalPath); + if (!blurPath.isEmpty()) { + sourcePath = blurPath; + } + } + + QStringList results = getCachedImagePaths(sourcePath, sizes); + if (!results.isEmpty()) { + return results; + } + + return QStringList() << sourcePath; +} + +QString CachedWallpaper::blurOutputPath(const QString &pathMd5, const QString &originalPath) +{ + QFileInfo fi(originalPath); + QString suffix = fi.suffix(); + return QString("%1/%2%3").arg(kBlurCacheDir, pathMd5, suffix.isEmpty() ? "" : "." + suffix); +} + +QString CachedWallpaper::generateBlurImage(const QString &pathMd5, const QString &originalPath) +{ + QFileInfo originalFileInfo(originalPath); + QString outputFile = blurOutputPath(pathMd5, originalPath); + + // Return cached file if up-to-date + QFileInfo outputFileInfo(outputFile); + if (outputFileInfo.exists() + && outputFileInfo.lastModified() >= originalFileInfo.lastModified() + && outputFileInfo.size() > 0) { + qDebug() << "Blur image already exists and up-to-date:" << outputFile; + return outputFile; + } + + qDebug() << "Generating blur image:" << originalPath << "=>" << outputFile; + + QImage blurredImage = ImageEffectProcessor::applyPixmixEffect(originalPath); + if (blurredImage.isNull()) { + qWarning() << "Failed to generate blur image:" << originalPath; + return QString(); + } + + if (!blurredImage.save(outputFile, QImageReader::imageFormat(originalPath), 100)) { + qWarning() << "Failed to save blur image:" << outputFile; + return QString(); + } + + // Match output mtime to input for cache freshness check + QFile timeFile(outputFile); + if (timeFile.open(QIODevice::ReadWrite)) { + timeFile.setFileTime(originalFileInfo.lastModified(), QFileDevice::FileModificationTime); + timeFile.close(); + } + + qDebug() << "Successfully generated blur image:" << outputFile; + return outputFile; +} + +QStringList CachedWallpaper::getWallpaperListForScreen(const QString &originalPath, const QList &sizes, bool needBlur) +{ + if (needBlur) { + QString blurPath = getBlurImagePath(originalPath); + if (!blurPath.isEmpty()) { + QStringList results = getCachedImagePaths(blurPath, sizes); + if (!results.isEmpty()) { + return results; + } + return QStringList() << blurPath; + } + qWarning() << "Blur processing failed for:" << originalPath << ", fallback to original"; + } + + QStringList results = getCachedImagePaths(originalPath, sizes); + if (!results.isEmpty()) { + return results; + } + + return QStringList() << originalPath; +} + +bool CachedWallpaper::deleteBlurImage(const QString &originalPath) +{ + QString pathMd5 = QCryptographicHash::hash(originalPath.toUtf8(), QCryptographicHash::Md5).toHex(); + + auto it = m_blurImageCache.find(pathMd5); + if (it != m_blurImageCache.end()) { + QString blurPath = it.value(); + m_blurImageCache.erase(it); + if (QFile::remove(blurPath)) { + qDebug() << "Deleted blur image:" << blurPath; + return true; + } + qWarning() << "Failed to delete blur image file:" << blurPath; + return false; + } + + // Try to delete on-disk file even if not in memory cache + QString outputFile = blurOutputPath(pathMd5, originalPath); + if (QFile::remove(outputFile)) { + qDebug() << "Deleted blur image file:" << outputFile; + } + return true; +} + +void CachedWallpaper::clearBlurCache() +{ + m_blurImageCache.clear(); + + QDir cacheDir(kBlurCacheDir); + if (!cacheDir.exists()) { + return; + } + + QFileInfoList files = cacheDir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); + int deletedCount = 0; + for (const QFileInfo &fileInfo : files) { + if (QFile::remove(fileInfo.absoluteFilePath())) { + deletedCount++; + } else { + qWarning() << "Failed to delete blur cache file:" << fileInfo.absoluteFilePath(); + } + } + qDebug() << "Cleared blur cache, deleted" << deletedCount << "files"; +} + +bool CachedWallpaper::deleteEffectImage(const QString &originalPath, const QString &effect) +{ + if (effect.isEmpty() || effect == "pixmix") { + return deleteBlurImage(originalPath); + } + qWarning() << "Unsupported effect for deletion:" << effect; + return false; +} + +void CachedWallpaper::clearEffectCache(const QString &effect) +{ + if (effect == "all") { + clearBlurCache(); + return; + } + + QString effectName = effect.isEmpty() ? "pixmix" : effect; + if (effectName == "pixmix") { + clearBlurCache(); + } else { + qWarning() << "Unsupported effect for cache clearing:" << effectName; + } +} diff --git a/src/plugin-qt/wallpapercache/cachedwallpaper.h b/src/plugin-qt/wallpapercache/cachedwallpaper.h new file mode 100644 index 00000000..6bc97fea --- /dev/null +++ b/src/plugin-qt/wallpapercache/cachedwallpaper.h @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef CACHED_WALLPAPER_H +#define CACHED_WALLPAPER_H + +#include +#include +#include +#include +#include + +// Shared cache path constants +inline const QString kWallpaperCacheDir = QStringLiteral("/var/cache/dde-wallpaper-cache"); +inline const QString kBlurCacheDir = kWallpaperCacheDir + QStringLiteral("/blur"); + +class CachedWallpaper : public QObject +{ + Q_OBJECT + +private: + explicit CachedWallpaper(); + ~CachedWallpaper(); + +signals: + void needHandleImage(const QString &originalPath, const QList &size, bool isMd5Path); + +public: + static CachedWallpaper *instance(); + QStringList getCachedImagePaths(const QString &originalPath, const QList &sizes, bool isMd5Path = false); + void cacheImage(const QString &originalPathMd5, const QString &size, const QString &processedPath); + + // Blur wallpaper interfaces + QString getBlurImagePath(const QString &originalPath); + QStringList getProcessedImageWithBlur(const QString &originalPath, const QList &sizes, bool needBlur = false); + + // Unified wallpaper processing interface + QStringList getWallpaperListForScreen(const QString &originalPath, const QList &sizes, bool needBlur = true); + + // Blur image management interfaces + bool deleteBlurImage(const QString &originalPath); + bool deleteEffectImage(const QString &originalPath, const QString &effect); + void clearBlurCache(); + void clearEffectCache(const QString &effect); + + static QString blurOutputPath(const QString &pathMd5, const QString &originalPath); + +private: + QString generateBlurImage(const QString &pathMd5, const QString &originalPath); + void cacheBlurImage(const QString &originalPathMd5, const QString &blurPath); + +private: + // originalPath's md5; sizeToString;processedWallpaperPath + QMap> m_cachedImages; + // 模糊壁纸缓存: originalPath's md5; blurImagePath + QMap m_blurImageCache; + QMutex m_blurGenerateMutex; +}; + +#endif // CACHED_WALLPAPER_H diff --git a/src/plugin-qt/wallpapercache/imageeffectprocessor.cpp b/src/plugin-qt/wallpapercache/imageeffectprocessor.cpp new file mode 100644 index 00000000..849511f2 --- /dev/null +++ b/src/plugin-qt/wallpapercache/imageeffectprocessor.cpp @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "imageeffectprocessor.h" + +#include +#include +#include +#include +#include + +#include + +// pixmix algorithm parameters +#define PIXMIX_MATRIX 16 // Image sample size +#define PIXMIX_OPACITY 90 // Color opacity +#define PIXMIX_SATURATION 50 // Saturation +#define PIXMIX_BRIGHTNESS -60 // Brightness + +DGUI_USE_NAMESPACE + +ImageEffectProcessor::ImageEffectProcessor(QObject *parent) + : QObject(parent) +{ +} + +ImageEffectProcessor::~ImageEffectProcessor() +{ +} + +QImage ImageEffectProcessor::applyEffect(const QString &imagePath, EffectType effectType) +{ + switch (effectType) { + case PixmixEffect: + return processPixmixEffect(imagePath); + default: + qWarning() << "Unsupported effect type:" << effectType; + return QImage(); + } +} + +QImage ImageEffectProcessor::applyPixmixEffect(const QString &imagePath) +{ + return processPixmixEffect(imagePath); +} + +bool ImageEffectProcessor::isSupportedEffect(EffectType effectType) +{ + switch (effectType) { + case PixmixEffect: + return true; + default: + return false; + } +} + +ImageEffectProcessor::EffectType ImageEffectProcessor::effectTypeFromString(const QString &effectName) +{ + if (effectName.isEmpty() || effectName == "pixmix") { + return PixmixEffect; + } + + qWarning() << "Unknown effect name:" << effectName << ", fallback to PixmixEffect"; + return PixmixEffect; +} + +QImage ImageEffectProcessor::processPixmixEffect(const QString &imagePath) +{ + QImage originalImage; + if (!originalImage.load(imagePath)) { + qWarning() << "Failed to load image:" << imagePath; + return QImage(); + } + + QColor averageColor = calculateAverageColor(originalImage, PIXMIX_MATRIX); + + QColor adjustedColor = DGuiApplicationHelper::adjustColor(averageColor, + 0, // Hue shift + PIXMIX_SATURATION, // Saturation + PIXMIX_BRIGHTNESS, // Brightness + 0, 0, 0, 0); + + QImage resultImage = originalImage.convertToFormat(QImage::Format_ARGB32_Premultiplied); + QPainter painter(&resultImage); + painter.setRenderHints(painter.renderHints() | QPainter::SmoothPixmapTransform); + + adjustedColor.setAlpha(int(PIXMIX_OPACITY * 1.0 / 100 * 255)); + painter.fillRect(resultImage.rect(), adjustedColor); + + painter.end(); + + qDebug() << "Applied pixmix effect - original color:" << averageColor + << "adjusted color:" << adjustedColor + << "opacity:" << PIXMIX_OPACITY << "%"; + + return resultImage; +} + +QColor ImageEffectProcessor::calculateAverageColor(const QImage &image, int sampleSize) +{ + QImage sampledImage = image.scaled(sampleSize, sampleSize, + Qt::IgnoreAspectRatio, Qt::FastTransformation); + + int totalR = 0, totalG = 0, totalB = 0; + + for (int i = 0; i < sampleSize; ++i) { + for (int j = 0; j < sampleSize; ++j) { + QRgb rgb = sampledImage.pixel(i, j); + totalR += qRed(rgb); + totalG += qGreen(rgb); + totalB += qBlue(rgb); + } + } + + int pixelCount = sampleSize * sampleSize; + return QColor(totalR / pixelCount, totalG / pixelCount, totalB / pixelCount); +} diff --git a/src/plugin-qt/wallpapercache/imageeffectprocessor.h b/src/plugin-qt/wallpapercache/imageeffectprocessor.h new file mode 100644 index 00000000..373606e5 --- /dev/null +++ b/src/plugin-qt/wallpapercache/imageeffectprocessor.h @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef IMAGE_EFFECT_PROCESSOR_H +#define IMAGE_EFFECT_PROCESSOR_H + +#include +#include +#include +#include + +/** + * @brief Image effect processor + * + * Handles image effects such as blur and color adjustment. + * Currently implements the pixmix effect. + */ +class ImageEffectProcessor : public QObject +{ + Q_OBJECT + +public: + /** + * @brief Supported image effect types + */ + enum EffectType { + PixmixEffect, // pixmix blur effect + // Future effect types can be added here + // GaussianBlur, // Gaussian blur + // ColorAdjust, // Color adjustment + }; + + explicit ImageEffectProcessor(QObject *parent = nullptr); + ~ImageEffectProcessor(); + + /** + * @brief Apply the specified image effect + * @param imagePath Input image path + * @param effectType Effect type + * @return Processed image, null QImage on failure + */ + static QImage applyEffect(const QString &imagePath, EffectType effectType = PixmixEffect); + + /** + * @brief Apply pixmix effect (compatibility interface) + * @param imagePath Input image path + * @return Processed image + */ + static QImage applyPixmixEffect(const QString &imagePath); + + /** + * @brief Check if the specified effect is supported + * @param effectType Effect type + * @return Whether supported + */ + static bool isSupportedEffect(EffectType effectType); + + /** + * @brief Convert effect name string to EffectType + * @param effectName Effect name (e.g. "pixmix", "") + * @return Effect type, defaults to PixmixEffect if unsupported + */ + static EffectType effectTypeFromString(const QString &effectName); + +private: + /** + * @brief Built-in pixmix algorithm implementation + * @param imagePath Input image path + * @return Processed image + */ + static QImage processPixmixEffect(const QString &imagePath); + + /** + * @brief Calculate the average color of an image + * @param image Input image + * @param sampleSize Sample size (default 16x16) + * @return Average color + */ + static QColor calculateAverageColor(const QImage &image, int sampleSize = 16); +}; + +#endif // IMAGE_EFFECT_PROCESSOR_H diff --git a/src/plugin-qt/wallpapercache/misc/deepin-service-plugin@org.deepin.dde.WallpaperCache.service.d/override.conf b/src/plugin-qt/wallpapercache/misc/deepin-service-plugin@org.deepin.dde.WallpaperCache.service.d/override.conf new file mode 100644 index 00000000..089c06c5 --- /dev/null +++ b/src/plugin-qt/wallpapercache/misc/deepin-service-plugin@org.deepin.dde.WallpaperCache.service.d/override.conf @@ -0,0 +1,29 @@ +# Override configuration for wallpaper cache plugin +# Mirror the original dde-wallpaper-cache.service security settings +[Service] +User=deepin-daemon +Group=deepin-daemon + +# systemd 自动创建 /var/cache/dde-wallpaper-cache 并赋权给 deepin-daemon +CacheDirectory=dde-wallpaper-cache + +ProtectSystem=strict +InaccessiblePaths=-/etc/shadow +InaccessiblePaths=-/etc/NetworkManager/system-connections +InaccessiblePaths=-/etc/pam.d +InaccessiblePaths=-/usr/share/uadp/ + +PrivateMounts=yes +NoNewPrivileges=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectControlGroups=yes +PrivateTmp=yes +PrivateDevices=yes +PrivateNetwork=yes +PrivateUsers=yes +RestrictNamespaces=yes +LockPersonality=yes +RestrictRealtime=yes +RemoveIPC=yes +MemoryDenyWriteExecute=yes diff --git a/src/plugin-qt/wallpapercache/misc/org.deepin.dde.ImageBlur1.conf b/src/plugin-qt/wallpapercache/misc/org.deepin.dde.ImageBlur1.conf new file mode 100644 index 00000000..69dbb78a --- /dev/null +++ b/src/plugin-qt/wallpapercache/misc/org.deepin.dde.ImageBlur1.conf @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/plugin-qt/wallpapercache/misc/org.deepin.dde.ImageBlur1.service b/src/plugin-qt/wallpapercache/misc/org.deepin.dde.ImageBlur1.service new file mode 100644 index 00000000..e2b50c35 --- /dev/null +++ b/src/plugin-qt/wallpapercache/misc/org.deepin.dde.ImageBlur1.service @@ -0,0 +1,5 @@ +[D-BUS Service] +Name=org.deepin.dde.ImageBlur1 +Exec=/usr/bin/deepin-service-manager -n org.deepin.dde.WallpaperCache +User=deepin-daemon +SystemdService=deepin-service-plugin@org.deepin.dde.WallpaperCache.service diff --git a/src/plugin-qt/wallpapercache/misc/org.deepin.dde.ImageEffect1.conf b/src/plugin-qt/wallpapercache/misc/org.deepin.dde.ImageEffect1.conf new file mode 100644 index 00000000..7d6a2f59 --- /dev/null +++ b/src/plugin-qt/wallpapercache/misc/org.deepin.dde.ImageEffect1.conf @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugin-qt/wallpapercache/misc/org.deepin.dde.ImageEffect1.service b/src/plugin-qt/wallpapercache/misc/org.deepin.dde.ImageEffect1.service new file mode 100644 index 00000000..97757d90 --- /dev/null +++ b/src/plugin-qt/wallpapercache/misc/org.deepin.dde.ImageEffect1.service @@ -0,0 +1,5 @@ +[D-BUS Service] +Name=org.deepin.dde.ImageEffect1 +Exec=/usr/bin/deepin-service-manager -n org.deepin.dde.WallpaperCache +User=deepin-daemon +SystemdService=deepin-service-plugin@org.deepin.dde.WallpaperCache.service diff --git a/src/plugin-qt/wallpapercache/misc/org.deepin.dde.WallpaperCache.conf b/src/plugin-qt/wallpapercache/misc/org.deepin.dde.WallpaperCache.conf new file mode 100644 index 00000000..0bce5a5d --- /dev/null +++ b/src/plugin-qt/wallpapercache/misc/org.deepin.dde.WallpaperCache.conf @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugin-qt/wallpapercache/misc/org.deepin.dde.WallpaperCache.service b/src/plugin-qt/wallpapercache/misc/org.deepin.dde.WallpaperCache.service new file mode 100644 index 00000000..219a2994 --- /dev/null +++ b/src/plugin-qt/wallpapercache/misc/org.deepin.dde.WallpaperCache.service @@ -0,0 +1,5 @@ +[D-BUS Service] +Name=org.deepin.dde.WallpaperCache +Exec=/usr/bin/deepin-service-manager -n org.deepin.dde.WallpaperCache +User=deepin-daemon +SystemdService=deepin-service-plugin@org.deepin.dde.WallpaperCache.service diff --git a/src/plugin-qt/wallpapercache/misc/plugin-qt-wallpapercache.json b/src/plugin-qt/wallpapercache/misc/plugin-qt-wallpapercache.json new file mode 100644 index 00000000..f22ac47b --- /dev/null +++ b/src/plugin-qt/wallpapercache/misc/plugin-qt-wallpapercache.json @@ -0,0 +1,19 @@ +{ + "name": "org.deepin.dde.WallpaperCache", + "libPath": "libplugin-qt-wallpapercache.so", + "policyStartType": "OnDemand", + "pluginType": "qt", + "idleTime": 1, + "startDelay": 0, + "policy": [ + { + "path": "/org/deepin/dde/WallpaperCache" + }, + { + "path": "/org/deepin/dde/ImageEffect1" + }, + { + "path": "/org/deepin/dde/ImageBlur1" + } + ] + } diff --git a/src/plugin-qt/wallpapercache/plugin.cpp b/src/plugin-qt/wallpapercache/plugin.cpp new file mode 100644 index 00000000..77322a7a --- /dev/null +++ b/src/plugin-qt/wallpapercache/plugin.cpp @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "wallpapercacheservice.h" +#include "wallpapercache.h" + +#include +#include + +#include + +DCORE_USE_NAMESPACE + +static std::unique_ptr service; +static std::unique_ptr imageEffectService; +static std::unique_ptr imageBlurService; +static std::unique_ptr wallpaperCache; + +extern "C" int DSMRegister(const char *name, void *data) +{ + (void)name; + + DLogManager::registerJournalAppender(); + + auto connection = reinterpret_cast(data); + + const QString path = "/org/deepin/dde/WallpaperCache"; + const QString imageEffectInterface = "org.deepin.dde.ImageEffect1"; + const QString imageEffectPath = "/org/deepin/dde/ImageEffect1"; + const QString imageBlurInterface = "org.deepin.dde.ImageBlur1"; + const QString imageBlurPath = "/org/deepin/dde/ImageBlur1"; + + // Register WallpaperCache primary service object + service = std::make_unique(); + if (!connection->registerObject(path, service.get(), + QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals)) { + qWarning() << "Failed to register WallpaperCache dbus object"; + } + + // Register ImageEffect1 compatibility service + if (connection->registerService(imageEffectInterface)) { + imageEffectService = std::make_unique(service.get()); + if (!connection->registerObject(imageEffectPath, imageEffectService.get(), + QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals)) { + qWarning() << "Failed to register ImageEffect1 dbus object"; + } else { + qInfo() << "Successfully registered ImageEffect1 compatibility service"; + } + } else { + qWarning() << "Failed to register ImageEffect1 dbus service"; + } + + // Register ImageBlur1 compatibility service + if (connection->registerService(imageBlurInterface)) { + imageBlurService = std::make_unique(service.get()); + if (!connection->registerObject(imageBlurPath, imageBlurService.get(), + QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals)) { + qWarning() << "Failed to register ImageBlur1 dbus object"; + } else { + qInfo() << "Successfully registered ImageBlur1 compatibility service"; + } + } else { + qWarning() << "Failed to register ImageBlur1 dbus service"; + } + + // Initialize wallpaper cache (load existing cache, start scale thread) + wallpaperCache = std::make_unique(); + + return 0; +} + +extern "C" int DSMUnRegister(const char *name, void *data) +{ + (void)name; + (void)data; + + wallpaperCache.reset(); + imageBlurService.reset(); + imageEffectService.reset(); + service.reset(); + + return 0; +} diff --git a/src/plugin-qt/wallpapercache/scaleimagethread.cpp b/src/plugin-qt/wallpapercache/scaleimagethread.cpp new file mode 100644 index 00000000..5f3af0f9 --- /dev/null +++ b/src/plugin-qt/wallpapercache/scaleimagethread.cpp @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "scaleimagethread.h" + +#include +#include +#include +#include +#include +#include + +ScaleImageThread::ScaleImageThread(QObject *parent) + : QThread(parent) +{ +} + +ScaleImageThread::~ScaleImageThread() +{ + stopThread(); + wait(); +} + +void ScaleImageThread::stopThread() +{ + QMutexLocker locker(&m_mutex); + m_stop = true; + m_waitCondition.wakeOne(); +} + +void ScaleImageThread::setCachePath(const QString &path) +{ + m_cachePath = path; +} + +void ScaleImageThread::addTask(const QString &originalPath, const QSize &targetSize) +{ + if (!QFile::exists(originalPath)) { + qWarning() << "file not exists:" << originalPath; + return; + } + + TaskData task; + task.originalPath = originalPath; + task.targetSize = targetSize; + + QMutexLocker locker(&m_mutex); + if (!m_tasks.contains(task)) { + m_tasks.enqueue(task); + } + + if (isRunning()) { + m_waitCondition.wakeOne(); + } else { + start(); + } +} + +void ScaleImageThread::addTasks(const QString &originalPath, const QList &sizes, bool isMd5Path) +{ + if (!QFile::exists(originalPath)) { + qWarning() << "file not exists:" << originalPath; + return; + } + + QList tasks; + for (const QSize &size : sizes) { + TaskData data; + data.originalPath = originalPath; + data.targetSize = size; + data.isMd5Path = isMd5Path; + + tasks.append(data); + } + + QMutexLocker locker(&m_mutex); + for (const TaskData &task : tasks) { + if (!m_tasks.contains(task)) { + m_tasks.enqueue(task); + } + } + + if (isRunning()) { + m_waitCondition.wakeOne(); + } else { + start(); + } +} + +bool ScaleImageThread::isIdle() +{ + QMutexLocker locker(&m_mutex); + return m_tasks.isEmpty(); +} + +void ScaleImageThread::run() +{ + while (!m_stop) { + QMutexLocker locker(&m_mutex); + + if (m_tasks.isEmpty()) { + m_waitCondition.wait(&m_mutex); + } + + if (!m_tasks.isEmpty()) { + TaskData task = m_tasks.head(); + locker.unlock(); + + executeTask(task); + + QMutexLocker locker2(&m_mutex); + m_tasks.dequeue(); + } + } +} + +void ScaleImageThread::executeTask(const TaskData &task) +{ + qDebug() << "task info:" << task.originalPath << " sizes:" << task.targetSize; + auto pixmap = scaleImage(task); + if (pixmap.isNull()) { + qWarning() << "scale image failed:" << task.originalPath; + return; + } + + QString originalPathMd5 = pathMd5(task.originalPath, task.isMd5Path); + QString cachedFilePath = cacheImageToDisk(pixmap, task, originalPathMd5); + + if (!cachedFilePath.isEmpty()) { + Q_EMIT imageScaled(originalPathMd5, sizeToString(task.targetSize), cachedFilePath); + + if (task.isMd5Path) { + QFile file(task.originalPath); + if (file.exists()) { + file.remove(); + } + } + } +} + +QImage ScaleImageThread::scaleImage(const TaskData &task) +{ + qDebug() << "scale image info:" << task.originalPath << " targetSize:" << task.targetSize; + QImage image; + image.load(task.originalPath); + if (image.isNull()) { + qWarning() << "load image failed:" << task.originalPath; + return image; + } + + QSize size = task.targetSize; + image = image.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::FastTransformation); + image = image.copy(QRect((image.width() - size.width()) / 2, + (image.height() - size.height()) / 2, + size.width(), + size.height())); + + return image; +} + +QString ScaleImageThread::cacheImageToDisk(QImage &image, const TaskData &task, const QString &md5String) +{ + QFileInfo originalFileInfo(task.originalPath); + + QString format = originalFileInfo.suffix(); // TODO format is null ? + // md5_1920x1080.jpg + QString fileName = md5String + "_" + sizeToString(task.targetSize) + "." + format; + + QString filePath = m_cachePath + "/" + fileName; + if (image.save(filePath, format.toStdString().c_str(), 100)) { + // Set the timestamp of the saved file to the original image's timestamp + QFile file(filePath); + //file.setPermissions(QFile::ReadOwner | QFile::WriteOwner); + file.setFileTime(originalFileInfo.lastModified(), QFileDevice::FileModificationTime); + } else { + qWarning() << "save image failed:" << filePath; + filePath.clear(); + } + + return filePath; +} + +QString ScaleImageThread::sizeToString(const QSize &size) +{ + return QString("%1x%2").arg(size.width()).arg(size.height()); +} + +QString ScaleImageThread::pathMd5(const QString &path, bool isMd5Path) +{ + QString pathmd5; + if (isMd5Path) { + QString filename = path.split("/").last(); + pathmd5 = filename.split(".").first(); + } else { + pathmd5 = QCryptographicHash::hash(path.toUtf8(), QCryptographicHash::Md5).toHex(); + } + + return pathmd5; +} diff --git a/src/plugin-qt/wallpapercache/scaleimagethread.h b/src/plugin-qt/wallpapercache/scaleimagethread.h new file mode 100644 index 00000000..e6dee565 --- /dev/null +++ b/src/plugin-qt/wallpapercache/scaleimagethread.h @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef SCALE_IMAGE_THREAD_H +#define SCALE_IMAGE_THREAD_H + +#include +#include +#include +#include +#include + +class ScaleImageThread : public QThread +{ + Q_OBJECT +public: + explicit ScaleImageThread(QObject *parent = nullptr); + ~ScaleImageThread() override; + + void stopThread(); + void setCachePath(const QString &path); + void addTask(const QString &originalPath, const QSize &targetSize); + void addTasks(const QString &originalPath, const QList &sizes, bool isMd5Path); + bool isIdle(); + + static QString pathMd5(const QString &path, bool isMd5Path); + static QString sizeToString(const QSize &size); + +signals: + void imageScaled(const QString &originalPath, const QString &size, const QString &scaledPath); + +protected: + void run() override; + +private: + struct TaskData { + QString originalPath; + QSize targetSize; + bool isMd5Path = false; + + bool operator==(const TaskData &other) const + { + return (originalPath == other.originalPath) && (targetSize == other.targetSize) && (isMd5Path == other.isMd5Path); + } + }; + +private: + QImage scaleImage(const TaskData &task); + QString cacheImageToDisk(QImage &pixmap, const TaskData &task, const QString &md5); + + void executeTask(const TaskData &task); + +private: + QMutex m_mutex; + QWaitCondition m_waitCondition; + QQueue m_tasks; + + bool m_stop = false; + QString m_cachePath; +}; + +#endif // SCALE_IMAGE_THREAD_H diff --git a/src/plugin-qt/wallpapercache/tests/CMakeLists.txt b/src/plugin-qt/wallpapercache/tests/CMakeLists.txt new file mode 100644 index 00000000..4c073831 --- /dev/null +++ b/src/plugin-qt/wallpapercache/tests/CMakeLists.txt @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +cmake_minimum_required(VERSION 3.16) +project(test_wallpaper_cache) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTOMOC ON) + +find_package(Qt6 REQUIRED COMPONENTS Core DBus Gui) + +add_executable(test_wallpaper_cache test_wallpaper_cache.cpp) + +target_link_libraries(test_wallpaper_cache + Qt6::Core + Qt6::DBus + Qt6::Gui +) + +target_compile_definitions(test_wallpaper_cache PRIVATE QT_MESSAGELOGCONTEXT) + +message(STATUS "Test build: test_wallpaper_cache") +message(STATUS "Run: sudo ./test_wallpaper_cache [wallpaper_path]") diff --git a/src/plugin-qt/wallpapercache/tests/test_dbus.sh b/src/plugin-qt/wallpapercache/tests/test_dbus.sh new file mode 100644 index 00000000..f767d521 --- /dev/null +++ b/src/plugin-qt/wallpapercache/tests/test_dbus.sh @@ -0,0 +1,481 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +# +# SPDX-License-Identifier: LGPL-3.0-or-later +# + +# ============================================================================= +# dde-wallpaper-cache functional test script (D-Bus CLI version) +# Usage: sudo bash test_dbus.sh [wallpaper_image_path] +# ============================================================================= + +set -e + +# ---- Config ---- +WALLPAPER="${1:-/usr/share/backgrounds/default_background.jpg}" +SERVICE_WC="org.deepin.dde.WallpaperCache" +PATH_WC="/org/deepin/dde/WallpaperCache" +SERVICE_IE="org.deepin.dde.ImageEffect1" +PATH_IE="/org/deepin/dde/ImageEffect1" +SERVICE_IB="org.deepin.dde.ImageBlur1" +PATH_IB="/org/deepin/dde/ImageBlur1" +BLUR_CACHE_DIR="/var/cache/dde-wallpaper-cache/blur" +SIZE_CACHE_DIR="/var/cache/dde-wallpaper-cache" + +# Color output +GREEN="\033[0;32m"; RED="\033[0;31m"; YELLOW="\033[1;33m"; NC="\033[0m" +FAILURES=0 +pass() { echo -e "${GREEN}[PASS]${NC} $1"; } +fail() { echo -e "${RED}[FAIL]${NC} $1"; FAILURES=$((FAILURES + 1)); } +info() { echo -e "${YELLOW}[INFO]${NC} $1"; } +section() { echo -e "\n${YELLOW}======== $1 ========${NC}"; } + +# Extract the first string value from gdbus call output, e.g. ('value',) or (['v1', 'v2'],) +gdbus_extract_string() { + echo "$1" | grep -oP "(?<=')[^']+(?=')" | head -1 +} +gdbus_extract_strings() { + echo "$1" | grep -oP "(?<=')[^']+(?=')" +} + +# Check if a file is a valid image using the file command +check_valid_image() { + local filepath="$1" + file "$filepath" | grep -qiE "image|JPEG|PNG|BMP|GIF|TIFF|WebP" +} + +# Get image dimensions using identify (ImageMagick) if available +get_image_dimensions() { + local filepath="$1" + if command -v identify &>/dev/null; then + identify -format "%wx%h" "$filepath" 2>/dev/null + else + echo "" + fi +} + +# ---- Pre-checks ---- +section "Pre-checks" +if [ ! -f "$WALLPAPER" ]; then + fail "Test wallpaper file not found: $WALLPAPER" + exit 1 +fi +info "Using wallpaper: $WALLPAPER" + +if ! dbus-send --system --print-reply \ + --dest="$SERVICE_WC" "$PATH_WC" \ + org.freedesktop.DBus.Introspectable.Introspect &>/dev/null; then + fail "WallpaperCache service unreachable, please start dde-wallpaper-cache first" + exit 1 +fi +pass "WallpaperCache service reachable" + +if ! dbus-send --system --print-reply \ + --dest="$SERVICE_IE" "$PATH_IE" \ + org.freedesktop.DBus.Introspectable.Introspect &>/dev/null; then + fail "ImageEffect1 compatibility service unreachable" + exit 1 +fi +pass "ImageEffect1 compatibility service reachable" + +# ---- Test 1: ImageEffect1.Get() blur effect ---- +section "Test 1: ImageEffect1.Get() blur effect" +info "Calling ImageEffect1.Get(\"\", \"$WALLPAPER\")..." +BLUR_PATH=$(dbus-send --system --print-reply \ + --dest="$SERVICE_IE" "$PATH_IE" \ + "${SERVICE_IE}.Get" string:"" string:"$WALLPAPER" \ + 2>&1 | grep -oP '(?<=string ").*(?=")') + +if [ -z "$BLUR_PATH" ]; then + fail "Get() returned empty string" +else + info "Blur image path: $BLUR_PATH" + if [ -f "$BLUR_PATH" ] && [ -s "$BLUR_PATH" ]; then + SIZE=$(du -h "$BLUR_PATH" | cut -f1) + pass "Blur image file exists, size: $SIZE" + else + fail "Blur image file missing or empty: $BLUR_PATH" + fi + + # Validate file is a real image + if check_valid_image "$BLUR_PATH"; then + pass "Blur image is a valid image file" + else + fail "Blur image is not a valid image file" + fi + + # Validate dimensions match original + ORIG_DIM=$(get_image_dimensions "$WALLPAPER") + BLUR_DIM=$(get_image_dimensions "$BLUR_PATH") + if [ -n "$ORIG_DIM" ] && [ -n "$BLUR_DIM" ]; then + if [ "$ORIG_DIM" = "$BLUR_DIM" ]; then + pass "Blur image dimensions match original: $BLUR_DIM" + else + fail "Blur image dimensions mismatch: original=$ORIG_DIM blur=$BLUR_DIM" + fi + else + info "ImageMagick not available, skipping dimension check" + fi + + # Validate cached in correct directory + if echo "$BLUR_PATH" | grep -q "^$BLUR_CACHE_DIR"; then + pass "Blur image stored in correct cache directory" + else + fail "Blur image not in expected cache directory: $BLUR_CACHE_DIR" + fi +fi + +# ---- Test 2: Cache hit (second call should be faster) ---- +section "Test 2: Cache hit verification" +info "Second call to Get(), expecting cached response..." +T1=$(date +%s%3N) +BLUR_PATH2=$(dbus-send --system --print-reply \ + --dest="$SERVICE_IE" "$PATH_IE" \ + "${SERVICE_IE}.Get" string:"" string:"$WALLPAPER" \ + 2>&1 | grep -oP '(?<=string ").*(?=")') +T2=$(date +%s%3N) +ELAPSED=$((T2 - T1)) + +if [ "$BLUR_PATH" = "$BLUR_PATH2" ]; then + pass "Two calls returned the same path: $BLUR_PATH2 (${ELAPSED}ms)" + if [ "$ELAPSED" -lt 200 ]; then + pass "Cache hit, response time ${ELAPSED}ms < 200ms" + else + info "Response time ${ELAPSED}ms, may not have hit memory cache (first disk read)" + fi +else + fail "Two calls returned different paths" +fi + +# ---- Test 3: GetBlurImagePath() dedicated interface ---- +section "Test 3: WallpaperCache.GetBlurImagePath()" +BLUR_PATH3=$(dbus-send --system --print-reply \ + --dest="$SERVICE_WC" "$PATH_WC" \ + "${SERVICE_WC}.GetBlurImagePath" string:"$WALLPAPER" \ + 2>&1 | grep -oP '(?<=string ").*(?=")') + +if [ "$BLUR_PATH3" = "$BLUR_PATH" ]; then + pass "GetBlurImagePath matches ImageEffect1.Get result" +else + fail "Path mismatch: GetBlurImagePath=$BLUR_PATH3 Get=$BLUR_PATH" +fi + +# ---- Test 4: Get() with explicit "pixmix" effect name ---- +section "Test 4: ImageEffect1.Get(\"pixmix\") explicit effect" +BLUR_PIXMIX=$(dbus-send --system --print-reply \ + --dest="$SERVICE_IE" "$PATH_IE" \ + "${SERVICE_IE}.Get" string:"pixmix" string:"$WALLPAPER" \ + 2>&1 | grep -oP '(?<=string ").*(?=")') + +if [ -n "$BLUR_PIXMIX" ] && [ "$BLUR_PIXMIX" = "$BLUR_PATH" ]; then + pass "Get(\"pixmix\") returns same path as Get(\"\")" +else + fail "Get(\"pixmix\") result mismatch or empty: $BLUR_PIXMIX" +fi + +# ---- Test 5: Get() with unsupported effect ---- +section "Test 5: ImageEffect1.Get() unsupported effect" +BLUR_BAD=$(dbus-send --system --print-reply \ + --dest="$SERVICE_IE" "$PATH_IE" \ + "${SERVICE_IE}.Get" string:"unsupported_effect" string:"$WALLPAPER" \ + 2>&1 | grep -oP '(?<=string ").*(?=")') + +if [ -z "$BLUR_BAD" ]; then + pass "Get(\"unsupported_effect\") returned empty (expected)" +else + fail "Get(\"unsupported_effect\") should return empty, got: $BLUR_BAD" +fi + +# ---- Test 6: GetProcessedImagePaths() size scaling ---- +section "Test 6: WallpaperCache.GetProcessedImagePaths() size scaling" +info "Requesting 1920x1080 scaling..." +RAW_OUTPUT=$(gdbus call --system \ + --dest "$SERVICE_WC" \ + --object-path "$PATH_WC" \ + --method "${SERVICE_WC}.GetProcessedImagePaths" \ + "$WALLPAPER" \ + "[<(1920, 1080)>]" 2>&1) +SCALED_PATHS=$(gdbus_extract_string "$RAW_OUTPUT") + +if [ -n "$SCALED_PATHS" ]; then + info "First call returned: $SCALED_PATHS" + # Wait for async processing (first call may return original path) + sleep 3 + # Call again to check if cache was generated + RAW_OUTPUT2=$(gdbus call --system \ + --dest "$SERVICE_WC" \ + --object-path "$PATH_WC" \ + --method "${SERVICE_WC}.GetProcessedImagePaths" \ + "$WALLPAPER" \ + "[<(1920, 1080)>]" 2>&1) + SCALED_PATHS2=$(gdbus_extract_string "$RAW_OUTPUT2") + info "Second call returned: $SCALED_PATHS2" + if echo "$SCALED_PATHS2" | grep -q "_"; then + pass "Size cache generated (path contains _ size suffix)" + # Validate scaled image dimensions + if [ -f "$SCALED_PATHS2" ]; then + SCALED_DIM=$(get_image_dimensions "$SCALED_PATHS2") + if [ -n "$SCALED_DIM" ]; then + info "Scaled image actual dimensions: $SCALED_DIM" + if [ "$SCALED_DIM" = "1920x1080" ]; then + pass "Scaled image dimensions correct: 1920x1080" + else + fail "Scaled image dimensions wrong: expected 1920x1080, got $SCALED_DIM" + fi + fi + # Validate it's a real image + if check_valid_image "$SCALED_PATHS2"; then + pass "Scaled image is a valid image file" + else + fail "Scaled image is not a valid image file" + fi + fi + else + info "Size image still being generated or same as original" + fi +else + fail "GetProcessedImagePaths returned empty" +fi + +# ---- Test 7: GetProcessedImageWithBlur() blur+size in one step ---- +section "Test 7: WallpaperCache.GetProcessedImageWithBlur() blur+size" +RAW_OUTPUT=$(gdbus call --system \ + --dest "$SERVICE_WC" \ + --object-path "$PATH_WC" \ + --method "${SERVICE_WC}.GetProcessedImageWithBlur" \ + "$WALLPAPER" \ + "[<(1920, 1080)>]" \ + true 2>&1) +BLUR_SCALED_LIST=$(gdbus_extract_strings "$RAW_OUTPUT") + +if [ -n "$BLUR_SCALED_LIST" ]; then + BLUR_SCALED=$(echo "$BLUR_SCALED_LIST" | head -1) + info "Returned: $BLUR_SCALED" + if [ -f "$BLUR_SCALED" ]; then + pass "GetProcessedImageWithBlur returned file exists" + if check_valid_image "$BLUR_SCALED"; then + pass "Blur+scaled image is a valid image file" + else + fail "Blur+scaled image is not a valid image file" + fi + else + info "File may still be generating asynchronously: $BLUR_SCALED" + fi +else + fail "GetProcessedImageWithBlur returned empty" +fi + +# needBlur=false should return non-blur path +RAW_OUTPUT_NB=$(gdbus call --system \ + --dest "$SERVICE_WC" \ + --object-path "$PATH_WC" \ + --method "${SERVICE_WC}.GetProcessedImageWithBlur" \ + "$WALLPAPER" \ + "[<(1920, 1080)>]" \ + false 2>&1) +NO_BLUR_RESULTS=$(gdbus_extract_strings "$RAW_OUTPUT_NB") + +if [ -n "$NO_BLUR_RESULTS" ]; then + NO_BLUR_FIRST=$(echo "$NO_BLUR_RESULTS" | head -1) + if echo "$NO_BLUR_FIRST" | grep -qv "/blur/"; then + pass "needBlur=false returned path not in blur directory" + else + fail "needBlur=false returned path from blur directory: $NO_BLUR_FIRST" + fi +else + fail "GetProcessedImageWithBlur(needBlur=false) returned empty" +fi + +# ---- Test 8: ImageEffect1.Delete() delete cache ---- +section "Test 8: ImageEffect1.Delete() delete blur cache" +info "Deleting blur cache for $WALLPAPER..." +dbus-send --system --print-reply \ + --dest="$SERVICE_IE" "$PATH_IE" \ + "${SERVICE_IE}.Delete" string:"pixmix" string:"$WALLPAPER" \ + 2>&1 | tail -1 + +if [ -n "$BLUR_PATH" ] && [ ! -f "$BLUR_PATH" ]; then + pass "Blur cache file deleted" +elif [ -n "$BLUR_PATH" ] && [ -f "$BLUR_PATH" ]; then + fail "Blur cache file still exists (Delete did not take effect)" +else + info "Cannot verify deletion (original path empty)" +fi + +# ---- Test 9: Regenerate after delete ---- +section "Test 9: Regenerate after delete" +info "Calling Get() again to verify regeneration..." +BLUR_PATH_NEW=$(dbus-send --system --print-reply \ + --dest="$SERVICE_IE" "$PATH_IE" \ + "${SERVICE_IE}.Get" string:"" string:"$WALLPAPER" \ + 2>&1 | grep -oP '(?<=string ").*(?=")') + +if [ -n "$BLUR_PATH_NEW" ] && [ -f "$BLUR_PATH_NEW" ] && [ -s "$BLUR_PATH_NEW" ]; then + pass "Regeneration successful: $BLUR_PATH_NEW" +else + fail "Regeneration failed" +fi + +# ---- Test 10: Delete("all") deletes all effect caches ---- +section "Test 10: ImageEffect1.Delete(\"all\") delete all" +# Ensure blur exists first +dbus-send --system --print-reply \ + --dest="$SERVICE_IE" "$PATH_IE" \ + "${SERVICE_IE}.Get" string:"" string:"$WALLPAPER" &>/dev/null + +BLUR_BEFORE=$(dbus-send --system --print-reply \ + --dest="$SERVICE_IE" "$PATH_IE" \ + "${SERVICE_IE}.Get" string:"" string:"$WALLPAPER" \ + 2>&1 | grep -oP '(?<=string ").*(?=")') + +dbus-send --system --print-reply \ + --dest="$SERVICE_IE" "$PATH_IE" \ + "${SERVICE_IE}.Delete" string:"all" string:"$WALLPAPER" \ + 2>&1 | tail -1 + +sleep 0.2 +if [ -n "$BLUR_BEFORE" ] && [ ! -f "$BLUR_BEFORE" ]; then + pass "Delete(\"all\") removed blur cache file" +else + fail "Delete(\"all\") did not remove blur cache file: $BLUR_BEFORE" +fi + +# Restore for subsequent tests +dbus-send --system --print-reply \ + --dest="$SERVICE_IE" "$PATH_IE" \ + "${SERVICE_IE}.Get" string:"" string:"$WALLPAPER" &>/dev/null + +# ---- Test 11: ImageBlur1.Get() compatibility interface ---- +section "Test 11: ImageBlur1.Get() compatibility interface" +if dbus-send --system --print-reply \ + --dest="$SERVICE_IB" "$PATH_IB" \ + org.freedesktop.DBus.Introspectable.Introspect &>/dev/null; then + pass "ImageBlur1 compatibility service reachable" + + BLUR1_PATH=$(dbus-send --system --print-reply \ + --dest="$SERVICE_IB" "$PATH_IB" \ + "${SERVICE_IB}.Get" string:"$WALLPAPER" \ + 2>&1 | grep -oP '(?<=string ").*(?=")') + + if [ -n "$BLUR1_PATH" ] && [ -f "$BLUR1_PATH" ]; then + pass "ImageBlur1.Get() returned valid path: $BLUR1_PATH" + # Consistency check + BLUR_WC=$(dbus-send --system --print-reply \ + --dest="$SERVICE_WC" "$PATH_WC" \ + "${SERVICE_WC}.GetBlurImagePath" string:"$WALLPAPER" \ + 2>&1 | grep -oP '(?<=string ").*(?=")') + if [ "$BLUR1_PATH" = "$BLUR_WC" ]; then + pass "ImageBlur1.Get matches GetBlurImagePath" + else + fail "ImageBlur1.Get path mismatch: $BLUR1_PATH vs $BLUR_WC" + fi + else + fail "ImageBlur1.Get() returned empty or file not found" + fi +else + fail "ImageBlur1 compatibility service unreachable" +fi + +# ---- Test 12: ImageBlur1.Delete() compatibility interface ---- +section "Test 12: ImageBlur1.Delete()" +if [ -n "$BLUR1_PATH" ] && [ -f "$BLUR1_PATH" ]; then + dbus-send --system --print-reply \ + --dest="$SERVICE_IB" "$PATH_IB" \ + "${SERVICE_IB}.Delete" string:"$WALLPAPER" \ + 2>&1 | tail -1 + + sleep 0.2 + if [ ! -f "$BLUR1_PATH" ]; then + pass "ImageBlur1.Delete() file removed" + else + fail "ImageBlur1.Delete() file still exists" + fi + + # Restore + dbus-send --system --print-reply \ + --dest="$SERVICE_IB" "$PATH_IB" \ + "${SERVICE_IB}.Get" string:"$WALLPAPER" &>/dev/null +else + info "Skipping ImageBlur1.Delete() test (no blur image)" +fi + +# ---- Test 13: GetWallpaperListForScreen() with multiple sizes ---- +section "Test 13: WallpaperCache.GetWallpaperListForScreen() multi-size" +RAW_OUTPUT=$(gdbus call --system \ + --dest "$SERVICE_WC" \ + --object-path "$PATH_WC" \ + --method "${SERVICE_WC}.GetWallpaperListForScreen" \ + "$WALLPAPER" \ + "[<(1920, 1080)>, <(2560, 1440)>]" \ + true 2>&1) +WLFS_RESULTS=$(gdbus_extract_strings "$RAW_OUTPUT") + +if [ -n "$WLFS_RESULTS" ]; then + COUNT=$(echo "$WLFS_RESULTS" | wc -l) + pass "GetWallpaperListForScreen(needBlur=true) returned $COUNT path(s)" + echo "$WLFS_RESULTS" | while read -r p; do + info " -> $p" + done +else + fail "GetWallpaperListForScreen returned empty" +fi + +# needBlur=false +RAW_OUTPUT=$(gdbus call --system \ + --dest "$SERVICE_WC" \ + --object-path "$PATH_WC" \ + --method "${SERVICE_WC}.GetWallpaperListForScreen" \ + "$WALLPAPER" \ + "[<(1920, 1080)>, <(2560, 1440)>]" \ + false 2>&1) +WLFS_RESULTS2=$(gdbus_extract_strings "$RAW_OUTPUT") + +if [ -n "$WLFS_RESULTS2" ]; then + pass "GetWallpaperListForScreen(needBlur=false) returned results" +else + fail "GetWallpaperListForScreen(needBlur=false) returned empty" +fi + +# ---- Test 14: Non-existent file error handling ---- +section "Test 14: Non-existent file error handling" +FAKE_PATH="/tmp/nonexistent_wallpaper_12345.jpg" +RESULT_FAKE=$(dbus-send --system --print-reply \ + --dest="$SERVICE_IE" "$PATH_IE" \ + "${SERVICE_IE}.Get" string:"" string:"$FAKE_PATH" \ + 2>&1 | grep -oP '(?<=string ").*(?=")') + +if [ -z "$RESULT_FAKE" ]; then + pass "Get() with non-existent file returned empty (service not crashed)" +else + info "Get() with non-existent file returned: $RESULT_FAKE" +fi + +# Verify service is still alive after error +if dbus-send --system --print-reply \ + --dest="$SERVICE_WC" "$PATH_WC" \ + org.freedesktop.DBus.Introspectable.Introspect &>/dev/null; then + pass "Service still alive after error handling" +else + fail "Service crashed after error handling" +fi + +# ---- Cache directory status ---- +section "Cache directory status" +info "Blur cache dir: $BLUR_CACHE_DIR" +ls -lh "$BLUR_CACHE_DIR" 2>/dev/null || echo " (directory not found or no permission)" +info "Size cache dir: $SIZE_CACHE_DIR" +ls -lh "$SIZE_CACHE_DIR" 2>/dev/null | head -10 || echo " (directory not found or no permission)" + +# ---- Summary ---- +section "Test complete" +echo "Tip: observe service log with:" +echo " sudo journalctl -u dde-wallpaper-cache -f" + +if [ "$FAILURES" -gt 0 ]; then + echo -e "${RED}$FAILURES test(s) failed${NC}" + exit 1 +else + echo -e "${GREEN}All tests passed${NC}" + exit 0 +fi diff --git a/src/plugin-qt/wallpapercache/tests/test_wallpaper_cache.cpp b/src/plugin-qt/wallpapercache/tests/test_wallpaper_cache.cpp new file mode 100644 index 00000000..23ed8d4c --- /dev/null +++ b/src/plugin-qt/wallpapercache/tests/test_wallpaper_cache.cpp @@ -0,0 +1,438 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +// dde-wallpaper-cache functional test program +// Build: see tests/CMakeLists.txt +// Run: sudo ./test_wallpaper_cache [wallpaper_path] + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ---------- Color output ---------- +#define PASS "\033[0;32m[PASS]\033[0m " +#define FAIL "\033[0;31m[FAIL]\033[0m " +#define INFO "\033[1;33m[INFO]\033[0m " +#define SECT "\033[1;36m[====]\033[0m " + +static int g_pass = 0, g_fail = 0; + +static void checkTrue(bool cond, const QString &msg) +{ + if (cond) { qInfo().noquote() << QString(PASS) + msg; ++g_pass; } + else { qWarning().noquote() << QString(FAIL) + msg; ++g_fail; } +} + +// Pack QSize list into QVariantList expected by WallpaperCache D-Bus interfaces +static QVariantList makeSizeArray(const QList &sizes) +{ + QVariantList list; + for (const QSize &s : sizes) { + list << QVariant::fromValue(s); + } + return list; +} + +// Validate that a file is a loadable image and return its dimensions +static QSize validateImageFile(const QString &path, const QString &label) +{ + QImageReader reader(path); + QSize size = reader.size(); + bool canRead = reader.canRead(); + + checkTrue(canRead, QString("%1 is a valid image file").arg(label)); + if (canRead && size.isValid()) { + qInfo().noquote() << INFO << QString("%1 dimensions: %2x%3") + .arg(label).arg(size.width()).arg(size.height()); + } + return size; +} + +// ---------- Main test logic ---------- +void runTests(const QString &wallpaper) +{ + const QString SVC_WC = "org.deepin.dde.WallpaperCache"; + const QString PATH_WC = "/org/deepin/dde/WallpaperCache"; + const QString SVC_IE = "org.deepin.dde.ImageEffect1"; + const QString PATH_IE = "/org/deepin/dde/ImageEffect1"; + const QString SVC_IB = "org.deepin.dde.ImageBlur1"; + const QString PATH_IB = "/org/deepin/dde/ImageBlur1"; + const QString BLUR_CACHE_DIR = "/var/cache/dde-wallpaper-cache/blur"; + + QDBusInterface wcIface(SVC_WC, PATH_WC, SVC_WC, QDBusConnection::systemBus()); + QDBusInterface ieIface(SVC_IE, PATH_IE, SVC_IE, QDBusConnection::systemBus()); + + // Get original image dimensions for comparison + QImageReader origReader(wallpaper); + QSize origSize = origReader.size(); + + // ===================================================================== + qInfo().noquote() << QString(SECT) + "Pre-checks"; + checkTrue(QFile::exists(wallpaper), QString("Wallpaper file exists: ") + wallpaper); + checkTrue(wcIface.isValid(), "WallpaperCache service reachable"); + checkTrue(ieIface.isValid(), "ImageEffect1 compatibility service reachable"); + if (!wcIface.isValid() || !ieIface.isValid()) { + qCritical() << FAIL "Services unreachable, please start dde-wallpaper-cache first"; + return; + } + if (origSize.isValid()) { + qInfo().noquote() << INFO << "Original image dimensions:" << origSize; + } + + // ===================================================================== + qInfo().noquote() << QString(SECT) + "Test 1: ImageEffect1.Get() blur image generation"; + QString blurPath; + { + QElapsedTimer timer; timer.start(); + QDBusReply reply = ieIface.call("Get", QString(""), wallpaper); + qint64 elapsed = timer.elapsed(); + + checkTrue(reply.isValid(), QString("Get() call succeeded (%1ms)").arg(elapsed)); + blurPath = reply.value(); + qInfo().noquote() << INFO << "Blur image path:" << blurPath; + checkTrue(!blurPath.isEmpty(), "Get() returned non-empty path"); + checkTrue(QFile::exists(blurPath), "Blur image file exists on disk"); + checkTrue(QFileInfo(blurPath).size() > 0, "Blur image file is not empty"); + qInfo().noquote() << INFO << "File size:" + << QFileInfo(blurPath).size() / 1024 << "KB"; + + // Validate cache directory + checkTrue(blurPath.startsWith(BLUR_CACHE_DIR), + "Blur image stored in correct cache directory: " + BLUR_CACHE_DIR); + + // Validate it's a real image and check dimensions + QSize blurSize = validateImageFile(blurPath, "Blur image"); + if (origSize.isValid() && blurSize.isValid()) { + checkTrue(origSize == blurSize, + QString("Blur image dimensions match original: %1x%2") + .arg(blurSize.width()).arg(blurSize.height())); + } + } + + // ===================================================================== + qInfo().noquote() << QString(SECT) + "Test 2: Cache hit (second call should be faster)"; + { + QElapsedTimer timer; timer.start(); + QDBusReply reply2 = ieIface.call("Get", QString(""), wallpaper); + qint64 elapsed = timer.elapsed(); + + checkTrue(reply2.isValid() && !reply2.value().isEmpty(), "Second Get() succeeded"); + checkTrue(reply2.value() == blurPath, "Second call returned same path"); + checkTrue(elapsed < 500, QString("Cache hit response < 500ms, actual %1ms").arg(elapsed)); + qInfo().noquote() << INFO << "Second call time:" << elapsed << "ms"; + } + + // ===================================================================== + qInfo().noquote() << QString(SECT) + "Test 3: WallpaperCache.GetBlurImagePath()"; + { + QDBusReply reply = wcIface.call("GetBlurImagePath", wallpaper); + checkTrue(reply.isValid(), "GetBlurImagePath() call succeeded"); + checkTrue(reply.value() == blurPath, + "GetBlurImagePath matches ImageEffect1.Get result"); + qInfo().noquote() << INFO << "GetBlurImagePath returned:" << reply.value(); + } + + // ===================================================================== + qInfo().noquote() << QString(SECT) + "Test 4: Get(\"pixmix\") explicit effect name"; + { + QDBusReply reply = ieIface.call("Get", QString("pixmix"), wallpaper); + checkTrue(reply.isValid(), "Get(\"pixmix\") call succeeded"); + checkTrue(reply.value() == blurPath, + "Get(\"pixmix\") returns same path as Get(\"\")"); + } + + // ===================================================================== + qInfo().noquote() << QString(SECT) + "Test 5: Get() with unsupported effect"; + { + QDBusReply reply = ieIface.call("Get", QString("unsupported_effect"), wallpaper); + checkTrue(reply.isValid(), "Get(\"unsupported_effect\") call did not crash"); + checkTrue(reply.value().isEmpty(), + "Get(\"unsupported_effect\") returned empty (expected)"); + } + + // ===================================================================== + qInfo().noquote() << QString(SECT) + "Test 6: GetProcessedImagePaths() size scaling"; + { + QList sizes = {QSize(1920, 1080)}; + QVariantList sizeArray = makeSizeArray(sizes); + + QDBusReply reply = wcIface.call("GetProcessedImagePaths", + wallpaper, sizeArray); + checkTrue(reply.isValid(), "GetProcessedImagePaths() call succeeded"); + qInfo().noquote() << INFO << "First call returned:" << reply.value(); + + // Wait for async processing + qInfo().noquote() << INFO << "Waiting 3s for async scaling to complete..."; + QThread::sleep(3); + + QDBusReply reply2 = wcIface.call("GetProcessedImagePaths", + wallpaper, sizeArray); + checkTrue(reply2.isValid(), "Second GetProcessedImagePaths() call succeeded"); + qInfo().noquote() << INFO << "Second call returned:" << reply2.value(); + + bool hasScaled = false; + for (const QString &p : reply2.value()) { + if (p.contains("_") && QFile::exists(p)) { + hasScaled = true; + qInfo().noquote() << INFO << "Scaled cache file:" << p + << "size:" << QFileInfo(p).size() / 1024 << "KB"; + + // Validate scaled image dimensions + QSize scaledSize = validateImageFile(p, "Scaled image"); + if (scaledSize.isValid()) { + checkTrue(scaledSize == QSize(1920, 1080), + QString("Scaled image dimensions correct: %1x%2") + .arg(scaledSize.width()).arg(scaledSize.height())); + } + } + } + checkTrue(hasScaled, "Scaled cache file generated (path contains _ size marker)"); + } + + // ===================================================================== + qInfo().noquote() << QString(SECT) + "Test 7: GetProcessedImageWithBlur() blur+size in one step"; + { + QList sizes = {QSize(1920, 1080)}; + QVariantList sizeArray = makeSizeArray(sizes); + + QDBusReply reply = wcIface.call("GetProcessedImageWithBlur", + wallpaper, sizeArray, true); + checkTrue(reply.isValid(), "GetProcessedImageWithBlur(needBlur=true) call succeeded"); + QStringList results = reply.value(); + qInfo().noquote() << INFO << "Returned paths:" << results; + checkTrue(!results.isEmpty(), "Returned non-empty list"); + + if (!results.isEmpty() && QFile::exists(results.first())) { + validateImageFile(results.first(), "Blur+scaled image"); + } + + // needBlur=false should return non-blur path + QDBusReply replyNoBlur = wcIface.call("GetProcessedImageWithBlur", + wallpaper, sizeArray, false); + checkTrue(replyNoBlur.isValid(), "GetProcessedImageWithBlur(needBlur=false) call succeeded"); + QStringList noBlurResults = replyNoBlur.value(); + qInfo().noquote() << INFO << "needBlur=false returned:" << noBlurResults; + checkTrue(!noBlurResults.isEmpty() && !noBlurResults.first().contains("/blur/"), + "needBlur=false result not from blur directory"); + } + + // ===================================================================== + qInfo().noquote() << QString(SECT) + "Test 8: GetProcessedImagePathByFd() image via FD"; + { + QFile file(wallpaper); + if (file.open(QIODevice::ReadOnly)) { + int fd = file.handle(); + QDBusUnixFileDescriptor dbusFd(fd); + QString md5 = QCryptographicHash::hash(wallpaper.toUtf8(), + QCryptographicHash::Md5).toHex(); + QList sizes = {QSize(2560, 1440)}; + QVariantList sizeArray = makeSizeArray(sizes); + + QDBusReply reply = wcIface.call("GetProcessedImagePathByFd", + QVariant::fromValue(dbusFd), + md5, sizeArray); + checkTrue(reply.isValid(), "GetProcessedImagePathByFd() call succeeded"); + qInfo().noquote() << INFO << "Returned:" << reply.value(); + file.close(); + } else { + qWarning().noquote() << FAIL "Cannot open wallpaper file"; + ++g_fail; + } + } + + // ===================================================================== + qInfo().noquote() << QString(SECT) + "Test 9: GetProcessedImagePathByFdWithBlur()"; + { + QFile file(wallpaper); + if (file.open(QIODevice::ReadOnly)) { + int fd = file.handle(); + QDBusUnixFileDescriptor dbusFd(fd); + QString md5 = QCryptographicHash::hash(wallpaper.toUtf8(), + QCryptographicHash::Md5).toHex(); + QList sizes = {QSize(1920, 1080)}; + QVariantList sizeArray = makeSizeArray(sizes); + + QDBusReply reply = wcIface.call("GetProcessedImagePathByFdWithBlur", + QVariant::fromValue(dbusFd), + md5, sizeArray, true); + checkTrue(reply.isValid(), "GetProcessedImagePathByFdWithBlur(needBlur=true) call succeeded"); + qInfo().noquote() << INFO << "Returned:" << reply.value(); + file.close(); + } else { + ++g_fail; + } + } + + // ===================================================================== + qInfo().noquote() << QString(SECT) + "Test 10: ImageEffect1.Delete() delete and rebuild"; + { + // Ensure file exists first + QDBusReply before = ieIface.call("Get", QString(""), wallpaper); + QString delBlurPath = before.value(); + bool existsBefore = QFile::exists(delBlurPath); + checkTrue(existsBefore, "Blur image exists before delete: " + delBlurPath); + + // Delete + QDBusPendingReply<> delReply = ieIface.asyncCall("Delete", QString("pixmix"), wallpaper); + delReply.waitForFinished(); + checkTrue(!delReply.isError(), "Delete() call no error"); + + QThread::msleep(200); + bool existsAfter = QFile::exists(delBlurPath); + checkTrue(!existsAfter, "File deleted after Delete()"); + + // Regenerate + QDBusReply regen = ieIface.call("Get", QString(""), wallpaper); + checkTrue(regen.isValid() && QFile::exists(regen.value()), + "Regenerated blur image after delete"); + qInfo().noquote() << INFO << "Regenerated path:" << regen.value(); + } + + // ===================================================================== + qInfo().noquote() << QString(SECT) + "Test 11: Delete(\"all\") deletes all effect caches"; + { + // Ensure blur image exists + QDBusReply before = ieIface.call("Get", QString(""), wallpaper); + QString allBlurPath = before.value(); + checkTrue(QFile::exists(allBlurPath), "Blur image exists before Delete(\"all\")"); + + QDBusPendingReply<> delAll = ieIface.asyncCall("Delete", QString("all"), wallpaper); + delAll.waitForFinished(); + checkTrue(!delAll.isError(), "Delete(\"all\") call no error"); + + QThread::msleep(200); + checkTrue(!QFile::exists(allBlurPath), + "Delete(\"all\") actually removed the blur cache file"); + + // Restore + ieIface.call("Get", QString(""), wallpaper); + } + + // ===================================================================== + qInfo().noquote() << QString(SECT) + "Test 12: Non-existent file error handling"; + { + QString fakePath = "/tmp/nonexistent_wallpaper_12345.jpg"; + + QDBusReply reply = ieIface.call("Get", QString(""), fakePath); + qInfo().noquote() << INFO << "Non-existent file Get() result:" << reply.value() + << " (error:" << reply.error().message() << ")"; + checkTrue(true, "Service did not crash on non-existent file"); + + // Verify service is still alive + QDBusReply alive = wcIface.call("GetBlurImagePath", wallpaper); + checkTrue(alive.isValid(), "Service still responsive after error handling"); + } + + // ===================================================================== + qInfo().noquote() << QString(SECT) + "Test 13: ImageBlur1.Get() compatibility interface"; + { + QDBusInterface ibIface(SVC_IB, PATH_IB, SVC_IB, QDBusConnection::systemBus()); + checkTrue(ibIface.isValid(), "ImageBlur1 compatibility service reachable"); + + if (ibIface.isValid()) { + QDBusReply reply = ibIface.call("Get", wallpaper); + checkTrue(reply.isValid(), "ImageBlur1.Get() call succeeded"); + QString ibBlurPath = reply.value(); + qInfo().noquote() << INFO << "ImageBlur1.Get returned:" << ibBlurPath; + checkTrue(!ibBlurPath.isEmpty(), "ImageBlur1.Get() returned non-empty path"); + checkTrue(QFile::exists(ibBlurPath), "ImageBlur1.Get() file exists"); + + // Consistency check with WallpaperCache.GetBlurImagePath + QDBusReply wcReply = wcIface.call("GetBlurImagePath", wallpaper); + checkTrue(ibBlurPath == wcReply.value(), + "ImageBlur1.Get matches GetBlurImagePath"); + } + } + + // ===================================================================== + qInfo().noquote() << QString(SECT) + "Test 14: ImageBlur1.Delete() compatibility interface"; + { + QDBusInterface ibIface(SVC_IB, PATH_IB, SVC_IB, QDBusConnection::systemBus()); + + if (ibIface.isValid()) { + // Ensure blur image exists + QDBusReply before = ibIface.call("Get", wallpaper); + QString ibBlurPath = before.value(); + checkTrue(QFile::exists(ibBlurPath), "Blur image exists before ImageBlur1.Delete"); + + // Delete + QDBusPendingReply<> delReply = ibIface.asyncCall("Delete", wallpaper); + delReply.waitForFinished(); + checkTrue(!delReply.isError(), "ImageBlur1.Delete() call no error"); + + QThread::msleep(200); + checkTrue(!QFile::exists(ibBlurPath), "ImageBlur1.Delete() file removed"); + + // Restore + ibIface.call("Get", wallpaper); + } + } + + // ===================================================================== + qInfo().noquote() << QString(SECT) + "Test 15: GetWallpaperListForScreen() multi-size"; + { + QList sizes = {QSize(1920, 1080), QSize(2560, 1440)}; + QVariantList sizeArray = makeSizeArray(sizes); + + QDBusReply reply = wcIface.call("GetWallpaperListForScreen", + wallpaper, sizeArray, true); + checkTrue(reply.isValid(), "GetWallpaperListForScreen(needBlur=true) call succeeded"); + QStringList results = reply.value(); + qInfo().noquote() << INFO << "Returned list:" << results; + checkTrue(!results.isEmpty(), "GetWallpaperListForScreen returned non-empty list"); + + // Validate each returned file + for (const QString &p : results) { + if (QFile::exists(p)) { + validateImageFile(p, "WallpaperListForScreen item"); + } + } + + // needBlur=false + QDBusReply reply2 = wcIface.call("GetWallpaperListForScreen", + wallpaper, sizeArray, false); + checkTrue(reply2.isValid(), "GetWallpaperListForScreen(needBlur=false) call succeeded"); + qInfo().noquote() << INFO << "needBlur=false returned:" << reply2.value(); + } + + // ===================================================================== + qInfo().noquote() << QString(SECT) + "Summary"; + qInfo().noquote() << QString(" Passed: %1 Failed: %2 Total: %3").arg(g_pass).arg(g_fail).arg(g_pass+g_fail); + if (g_fail == 0) + qInfo().noquote() << QString(PASS) + "All tests passed!"; + else + qWarning().noquote() << QString(FAIL) + QString("%1 test(s) failed").arg(g_fail); +} + +// ---------- main ---------- +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + + QString wallpaper = argc > 1 ? argv[1] : "/usr/share/backgrounds/default_background.jpg"; + qInfo().noquote() << "\n" << INFO << "dde-wallpaper-cache functional test"; + qInfo().noquote() << INFO << "Test wallpaper:" << wallpaper; + qInfo().noquote() << INFO << "Tip: observe log with: sudo journalctl -u dde-wallpaper-cache -f\n"; + + QTimer::singleShot(0, &app, [&]() { + runTests(wallpaper); + QCoreApplication::exit(g_fail > 0 ? 1 : 0); + }); + + return app.exec(); +} diff --git a/src/plugin-qt/wallpapercache/wallpapercache.cpp b/src/plugin-qt/wallpapercache/wallpapercache.cpp new file mode 100644 index 00000000..491a48f7 --- /dev/null +++ b/src/plugin-qt/wallpapercache/wallpapercache.cpp @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "wallpapercache.h" +#include "scaleimagethread.h" +#include "cachedwallpaper.h" + +#include +#include +#include + +WallpaperCache::WallpaperCache(QObject *parent) + : QObject(parent) + , m_scaleImageThread(new ScaleImageThread(this)) +{ + m_scaleImageThread->setCachePath(kWallpaperCacheDir); + + // Load existing cached wallpaper info + readCachedWallpaper(); + + connect(CachedWallpaper::instance(), &CachedWallpaper::needHandleImage, + m_scaleImageThread, &ScaleImageThread::addTasks, Qt::QueuedConnection); + connect(m_scaleImageThread, &ScaleImageThread::imageScaled, + CachedWallpaper::instance(), &CachedWallpaper::cacheImage); + + // Lifecycle managed by deepin-service-manager when running as plugin. +} + +WallpaperCache::~WallpaperCache() +{ + m_scaleImageThread->stopThread(); + m_scaleImageThread->wait(); +} + +void WallpaperCache::readCachedWallpaper() +{ + // Create cache directory if it does not exist + QDir dir(kWallpaperCacheDir); + if (!dir.exists()) { + if (!dir.mkpath(kWallpaperCacheDir)) { + qWarning() << "Failed to create directory:" << kWallpaperCacheDir; + } + return; + } + + // Regular expression to match md5, sizeToString, and file extension + QRegularExpression reg("^(\\w+)_(\\d+x\\d+)\\.(\\w+)$"); + + // Scan cache directory and restore cached entries + QFileInfoList fileList = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); + for (const QFileInfo &fileInfo : fileList) { + QString fileName = fileInfo.fileName(); + QString filePath = fileInfo.filePath(); + + QRegularExpressionMatch match = reg.match(fileName); + if (!match.hasMatch()) { + continue; + } + + QString md5 = match.captured(1); + QString sizeToString = match.captured(2); + + CachedWallpaper::instance()->cacheImage(md5, sizeToString, filePath); + } +} diff --git a/src/plugin-qt/wallpapercache/wallpapercache.h b/src/plugin-qt/wallpapercache/wallpapercache.h new file mode 100644 index 00000000..1df119b6 --- /dev/null +++ b/src/plugin-qt/wallpapercache/wallpapercache.h @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef WALLPAPER_CACHE_H +#define WALLPAPER_CACHE_H + +#include + +class ScaleImageThread; + +class WallpaperCache : public QObject +{ + Q_OBJECT +public: + explicit WallpaperCache(QObject *parent = nullptr); + ~WallpaperCache() override; + +private: + void readCachedWallpaper(); + +private: + ScaleImageThread *m_scaleImageThread; +}; + +#endif // WALLPAPER_CACHE_H diff --git a/src/plugin-qt/wallpapercache/wallpapercachemanager.cpp b/src/plugin-qt/wallpapercache/wallpapercachemanager.cpp new file mode 100644 index 00000000..5db5e101 --- /dev/null +++ b/src/plugin-qt/wallpapercache/wallpapercachemanager.cpp @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "wallpapercachemanager.h" +#include "cachedwallpaper.h" + +#include +#include +#include +#include + +WallpaperCacheManager::WallpaperCacheManager(QObject *parent) + : QObject(parent) +{ +} + +WallpaperCacheManager::~WallpaperCacheManager() +{ +} + +WallpaperCacheManager *WallpaperCacheManager::instance() +{ + static WallpaperCacheManager instance; + return &instance; +} + +bool WallpaperCacheManager::deleteBlurImage(const QString &originalPath) +{ + return CachedWallpaper::instance()->deleteBlurImage(originalPath); +} + +void WallpaperCacheManager::clearBlurCache() +{ + CachedWallpaper::instance()->clearBlurCache(); +} + +void WallpaperCacheManager::clearEffectCache(const QString &effect) +{ + CachedWallpaper::instance()->clearEffectCache(effect); +} + +qint64 WallpaperCacheManager::getCacheSize(const QString &type) +{ + qint64 totalSize = 0; + + if (type == "all" || type == "blur" || type == "pixmix") { + QDir blurDir(kBlurCacheDir); + if (blurDir.exists()) { + QFileInfoList files = blurDir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); + for (const QFileInfo &fileInfo : files) { + totalSize += fileInfo.size(); + } + } + } + + // Extensible: add other cache type size calculation here + + return totalSize; +} + +int WallpaperCacheManager::getCacheCount(const QString &type) +{ + int totalCount = 0; + + if (type == "all" || type == "blur" || type == "pixmix") { + QDir blurDir(kBlurCacheDir); + if (blurDir.exists()) { + QFileInfoList files = blurDir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); + totalCount += files.count(); + } + } + + return totalCount; +} + +QStringList WallpaperCacheManager::getCacheList(const QString &type) +{ + QStringList cacheList; + + if (type == "all" || type == "blur" || type == "pixmix") { + QDir blurDir(kBlurCacheDir); + if (blurDir.exists()) { + QFileInfoList files = blurDir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); + for (const QFileInfo &fileInfo : files) { + cacheList << fileInfo.absoluteFilePath(); + } + } + } + + return cacheList; +} + +bool WallpaperCacheManager::isBlurImageCached(const QString &originalPath) +{ + // Check disk only — do not trigger generation via getBlurImagePath + QString pathMd5 = QCryptographicHash::hash(originalPath.toUtf8(), QCryptographicHash::Md5).toHex(); + return QFile::exists(CachedWallpaper::blurOutputPath(pathMd5, originalPath)); +} + +QString WallpaperCacheManager::getBlurImagePath(const QString &originalPath) +{ + return CachedWallpaper::instance()->getBlurImagePath(originalPath); +} diff --git a/src/plugin-qt/wallpapercache/wallpapercachemanager.h b/src/plugin-qt/wallpapercache/wallpapercachemanager.h new file mode 100644 index 00000000..5ac9af9f --- /dev/null +++ b/src/plugin-qt/wallpapercache/wallpapercachemanager.h @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef WALLPAPER_CACHE_MANAGER_H +#define WALLPAPER_CACHE_MANAGER_H + +#include + +/** + * @brief Wallpaper cache manager + * + * Provides cache management functions for internal use and CLI tools. + * Does not export D-Bus interfaces. + */ +class WallpaperCacheManager : public QObject +{ + Q_OBJECT + +public: + explicit WallpaperCacheManager(QObject *parent = nullptr); + ~WallpaperCacheManager(); + + static WallpaperCacheManager *instance(); + + // Cache management + bool deleteBlurImage(const QString &originalPath); + void clearBlurCache(); + void clearEffectCache(const QString &effect = "all"); + + // Cache statistics + qint64 getCacheSize(const QString &type = "all"); + int getCacheCount(const QString &type = "all"); + QStringList getCacheList(const QString &type = "all"); + + // Cache status + bool isBlurImageCached(const QString &originalPath); + QString getBlurImagePath(const QString &originalPath); +}; + +#endif // WALLPAPER_CACHE_MANAGER_H diff --git a/src/plugin-qt/wallpapercache/wallpapercacheservice.cpp b/src/plugin-qt/wallpapercache/wallpapercacheservice.cpp new file mode 100644 index 00000000..5178b0a1 --- /dev/null +++ b/src/plugin-qt/wallpapercache/wallpapercacheservice.cpp @@ -0,0 +1,215 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "wallpapercacheservice.h" +#include "cachedwallpaper.h" +#include "wallpapercachemanager.h" + +#include +#include +#include +#include +#include + +#include + +WallpaperCacheService::WallpaperCacheService(QObject *parent) + : QObject(parent) +{ +} + +QList WallpaperCacheService::parseSizeArray(const QVariantList &sizeArray) +{ + QList sizes; + for (const QVariant &variant : sizeArray) { + auto arg = variant.value(); + QSize size; + arg >> size; + sizes.append(size); + } + return sizes; +} + +QStringList WallpaperCacheService::GetProcessedImagePaths(const QString &originalPath, const QVariantList &sizeArray) +{ + if (!QFile::exists(originalPath)) { + return QStringList() << originalPath; + } + + qDebug() << "get processed image from origin path:" << originalPath; + QList sizes = parseSizeArray(sizeArray); + + QStringList results = CachedWallpaper::instance()->getCachedImagePaths(originalPath, sizes); + if (results.isEmpty()) { + results << originalPath; + } + + return results; +} + +QStringList WallpaperCacheService::GetProcessedImagePathByFd(const QDBusUnixFileDescriptor &fd, const QString &imagePathMd5, const QVariantList &sizeArray) +{ + QString destinationPath = saveImageFromFd(fd, imagePathMd5); + if (destinationPath.isEmpty()) { + return QStringList(); + } + + qDebug() << "get processed image from origin path:" << destinationPath; + QList sizes = parseSizeArray(sizeArray); + + QStringList results = CachedWallpaper::instance()->getCachedImagePaths(destinationPath, sizes, true); + if (results.isEmpty()) { + results << destinationPath; + } + + return results; +} + +QString WallpaperCacheService::GetBlurImagePath(const QString &originalPath) +{ + if (!QFile::exists(originalPath)) { + qWarning() << "Original image not exists:" << originalPath; + return QString(); + } + + return CachedWallpaper::instance()->getBlurImagePath(originalPath); +} + +QStringList WallpaperCacheService::GetProcessedImageWithBlur(const QString &originalPath, const QVariantList &sizeArray, bool needBlur) +{ + if (!QFile::exists(originalPath)) { + qWarning() << "Original image not exists:" << originalPath; + return QStringList() << originalPath; + } + + QList sizes = parseSizeArray(sizeArray); + return CachedWallpaper::instance()->getProcessedImageWithBlur(originalPath, sizes, needBlur); +} + +QStringList WallpaperCacheService::GetProcessedImagePathByFdWithBlur(const QDBusUnixFileDescriptor &fd, const QString &imagePathMd5, const QVariantList &sizeArray, bool needBlur) +{ + QString destinationPath = saveImageFromFd(fd, imagePathMd5); + if (destinationPath.isEmpty()) { + return QStringList(); + } + + qDebug() << "get processed image from origin path:" << destinationPath; + QList sizes = parseSizeArray(sizeArray); + + if (needBlur) { + return CachedWallpaper::instance()->getProcessedImageWithBlur(destinationPath, sizes, needBlur); + } else { + QStringList results = CachedWallpaper::instance()->getCachedImagePaths(destinationPath, sizes, true); + if (results.isEmpty()) { + results << destinationPath; + } + return results; + } +} + +QStringList WallpaperCacheService::GetWallpaperListForScreen(const QString &originalPath, const QVariantList &sizeArray, bool needBlur) +{ + if (!QFile::exists(originalPath)) { + qWarning() << "Original image not exists:" << originalPath; + return QStringList() << originalPath; + } + + QList sizes = parseSizeArray(sizeArray); + return CachedWallpaper::instance()->getWallpaperListForScreen(originalPath, sizes, needBlur); +} + +QString WallpaperCacheService::Get(const QString &effect, const QString &filename) +{ + // Compatible with dde-daemon ImageEffect Get method. + // Currently only supports pixmix effect (blur). + + if (!QFile::exists(filename)) { + qWarning() << "Input file not exists:" << filename; + return QString(); + } + + QString trimmedEffect = effect.trimmed(); + if (trimmedEffect.isEmpty() || trimmedEffect == "pixmix") { + return CachedWallpaper::instance()->getBlurImagePath(filename); + } + + qWarning() << "Unsupported effect:" << effect << "only 'pixmix' is supported"; + return QString(); +} + +void WallpaperCacheService::Delete(const QString &effect, const QString &filename) +{ + // Compatible with dde-daemon ImageEffect Delete method. + QString normalizedEffect = effect.isEmpty() ? "pixmix" : effect; + + if (normalizedEffect == "all") { + QStringList supportedEffects = {"pixmix"}; + for (const QString &supportedEffect : supportedEffects) { + CachedWallpaper::instance()->deleteEffectImage(filename, supportedEffect); + } + } else { + bool success = CachedWallpaper::instance()->deleteEffectImage(filename, normalizedEffect); + if (!success) { + qWarning() << "Failed to delete effect image for effect:" << normalizedEffect + << "file:" << filename; + } + } +} + +bool WallpaperCacheService::DeleteBlurImage(const QString &originalPath) +{ + return WallpaperCacheManager::instance()->deleteBlurImage(originalPath); +} + +void WallpaperCacheService::ClearBlurCache() +{ + WallpaperCacheManager::instance()->clearBlurCache(); +} + +void WallpaperCacheService::ClearEffectCache(const QString &effect) +{ + WallpaperCacheManager::instance()->clearEffectCache(effect); +} + +QString WallpaperCacheService::saveImageFromFd(const QDBusUnixFileDescriptor &fd, const QString &imagePathMd5) +{ + int fdi = fd.fileDescriptor(); + if (fdi <= 0) { + return QString(); + } + + QString originPath = kWallpaperCacheDir + "/" + imagePathMd5; + QFile originFile(originPath); + if (!originFile.open(QIODevice::ReadWrite)) { + qWarning() << "Failed to open destination file for writing:" << originPath; + return QString(); + } + + char buffer[4096]; + ssize_t bytesRead; + while ((bytesRead = read(fdi, buffer, sizeof(buffer))) > 0) { + if (originFile.write(buffer, bytesRead) != bytesRead) { + qWarning() << "Failed to write data to destination file."; + break; + } + } + if (bytesRead < 0) { + qWarning() << "Error reading from file descriptor."; + originFile.close(); + return QString(); + } + originFile.close(); + + // Detect format and rename with extension + QImageReader reader(originPath); + QString format = reader.format(); + QString destinationPath = originPath + QString(".%1").arg(format); + QFile file(originPath); + if (!file.rename(destinationPath)) { + qWarning() << "originPath rename failed"; + return QString(); + } + + return destinationPath; +} diff --git a/src/plugin-qt/wallpapercache/wallpapercacheservice.h b/src/plugin-qt/wallpapercache/wallpapercacheservice.h new file mode 100644 index 00000000..7059374a --- /dev/null +++ b/src/plugin-qt/wallpapercache/wallpapercacheservice.h @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef WALLPAPER_CACHE_SERVICE_H +#define WALLPAPER_CACHE_SERVICE_H + +#include +#include + +class WallpaperCacheService : public QObject +{ + Q_OBJECT + // Primary D-Bus interface name (for introspection) + Q_CLASSINFO("D-Bus Interface", "org.deepin.dde.WallpaperCache") + // Note: org.deepin.dde.ImageEffect1 compatibility interface is mounted + // on a separate object path via plugin.cpp; not declared here to avoid + // overriding the primary interface name. + +public: + explicit WallpaperCacheService(QObject *parent = nullptr); + +public Q_SLOTS: + // Scale wallpaper to multiple screen sizes; returns cached paths if available, + // otherwise triggers async processing and immediately returns original path. + QStringList GetProcessedImagePaths(const QString &originalPath, const QVariantList &sizeArray); + // Same as above, but reads source image from a file descriptor. + QStringList GetProcessedImagePathByFd(const QDBusUnixFileDescriptor &fd, const QString &imagePathMd5, const QVariantList &sizeArray); + + // Synchronously generates and returns the blurred wallpaper path (no scaling). + QString GetBlurImagePath(const QString &originalPath); + + // Sync blur + async scaling; returns cached scaled paths if available, + // otherwise immediately returns the blurred (or original) image path. + QStringList GetProcessedImageWithBlur(const QString &originalPath, const QVariantList &sizeArray, bool needBlur); + // Same as above, but reads source image from a file descriptor; returns list. + QStringList GetProcessedImagePathByFdWithBlur(const QDBusUnixFileDescriptor &fd, const QString &imagePathMd5, const QVariantList &sizeArray, bool needBlur); + + // Sync blur + async multi-screen scaling; returns cached paths per size if available, + // otherwise immediately returns the blurred (or original) image path. + QStringList GetWallpaperListForScreen(const QString &originalPath, const QVariantList &sizeArray, bool needBlur = true); + + // ImageEffect compatibility interfaces (replaces dde-daemon ImageEffect service) + // ImageEffect1 compat: get processed image path for given effect (only "pixmix"/blur supported). + QString Get(const QString &effect, const QString &filename); + // ImageEffect1 compat: delete cached effect image; effect "all" removes all caches. + void Delete(const QString &effect, const QString &filename); + +public: + // Management interfaces (not exported via D-Bus, for internal/CLI use) + bool DeleteBlurImage(const QString &originalPath); + void ClearBlurCache(); + void ClearEffectCache(const QString &effect = "all"); + +private: + QString saveImageFromFd(const QDBusUnixFileDescriptor &fd, const QString &imagePathMd5); + static QList parseSizeArray(const QVariantList &sizeArray); +}; + +class ImageEffect1Service : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.deepin.dde.ImageEffect1") +public: + explicit ImageEffect1Service(WallpaperCacheService *target, QObject *parent = nullptr) + : QObject(parent), m_target(target) {} + +public Q_SLOTS: + QString Get(const QString &effect, const QString &filename) { + return m_target->Get(effect, filename); + } + void Delete(const QString &effect, const QString &filename) { + m_target->Delete(effect, filename); + } +private: + WallpaperCacheService *m_target; +}; + +class ImageBlur1Service : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.deepin.dde.ImageBlur1") +public: + explicit ImageBlur1Service(WallpaperCacheService *target, QObject *parent = nullptr) + : QObject(parent), m_target(target) {} + +public Q_SLOTS: + QString Get(const QString &filename) { + return m_target->GetBlurImagePath(filename); + } + void Delete(const QString &filename) { + m_target->DeleteBlurImage(filename); + } + +Q_SIGNALS: + void BlurDone(const QString &file, const QString &blurFile, bool ok); + +private: + WallpaperCacheService *m_target; +}; + +#endif // WALLPAPER_CACHE_SERVICE_H