diff --git a/libimageviewer/assets/frame/common/error.svg b/libimageviewer/assets/frame/common/error.svg
new file mode 100644
index 00000000..8ae60594
--- /dev/null
+++ b/libimageviewer/assets/frame/common/error.svg
@@ -0,0 +1,33 @@
+
+
\ No newline at end of file
diff --git a/libimageviewer/assets/frame/frametheme.qrc b/libimageviewer/assets/frame/frametheme.qrc
index a2a8e750..ca18066f 100755
--- a/libimageviewer/assets/frame/frametheme.qrc
+++ b/libimageviewer/assets/frame/frametheme.qrc
@@ -15,5 +15,6 @@
dark/images/importtip/close_hover.png
dark/images/importtip/close_normal.png
dark/images/importtip/close_press.png
+ common/error.svg
diff --git a/libimageviewer/assets/icons/texts/dcc_file_save_as_36px.svg b/libimageviewer/assets/icons/texts/dcc_file_save_as_36px.svg
new file mode 100644
index 00000000..8d8a00c4
--- /dev/null
+++ b/libimageviewer/assets/icons/texts/dcc_file_save_as_36px.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/libimageviewer/assets/icons/texts/dcc_reset_36px.svg b/libimageviewer/assets/icons/texts/dcc_reset_36px.svg
new file mode 100644
index 00000000..9b52b1eb
--- /dev/null
+++ b/libimageviewer/assets/icons/texts/dcc_reset_36px.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/libimageviewer/assets/icons/texts/dcc_save_36px.svg b/libimageviewer/assets/icons/texts/dcc_save_36px.svg
new file mode 100644
index 00000000..468fe32f
--- /dev/null
+++ b/libimageviewer/assets/icons/texts/dcc_save_36px.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/libimageviewer/assets/icons/theme-icons.qrc b/libimageviewer/assets/icons/theme-icons.qrc
index ac48381b..1385935a 100644
--- a/libimageviewer/assets/icons/theme-icons.qrc
+++ b/libimageviewer/assets/icons/theme-icons.qrc
@@ -26,5 +26,8 @@
texts/dcc_up_36px.svg
texts/dcc_down_dark_36px.svg
texts/dcc_down_36px.svg
+ texts/dcc_save_36px.svg
+ texts/dcc_file_save_as_36px.svg
+ texts/dcc_reset_36px.svg
diff --git a/libimageviewer/image-viewer_global.h b/libimageviewer/image-viewer_global.h
index 2beff3ed..5965ed48 100644
--- a/libimageviewer/image-viewer_global.h
+++ b/libimageviewer/image-viewer_global.h
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd.
+// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later
@@ -43,6 +43,7 @@ enum NormalMenuItemId {
IdSubMenu,
IdDraw,
IdOcr,
+ IdImageEnhance,
MenuItemCount
};
enum ItemInfoType {
diff --git a/libimageviewer/service/aimodelservice.cpp b/libimageviewer/service/aimodelservice.cpp
new file mode 100644
index 00000000..09783c07
--- /dev/null
+++ b/libimageviewer/service/aimodelservice.cpp
@@ -0,0 +1,710 @@
+// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "aimodelservice.h"
+#include "aimodelservice_p.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include "unionimage/unionimage.h"
+#include "service/commonservice.h"
+
+DWIDGET_USE_NAMESPACE
+
+// 超时限制10分钟
+static const int s_DBusTimeout = 1000 * 60 * 10;
+// 模型信息
+static const char *s_ModelColoring = "coloring";
+static const char *s_ModelSuperResol = "super-resolution";
+static const char *s_ModelBlurBkg = "blurredBackground";
+static const char *s_ModelBkgCut = "portraitCutout";
+static const char *s_ModelHand = "hand";
+static const char *s_ModelCartoon2D = "cartoon2d";
+static const char *s_ModelCartoon3D = "cartoon3d";
+static const char *s_ModelSketch = "sketch";
+// DBus
+static const QString s_EnhanceService = "com.deepin.imageenhance";
+static const QString s_EnhancePath = "/com/deepin/imageenhance";
+static const QString s_EnhanceInterface = "com.deepin.imageenhance";
+// DBus Interface
+static const QString s_EnhanceProcMethod = "imageEnhance";
+static const QString s_EnhanceBlurBkg = "blurredBackground";
+static const QString s_EnhancePortraitCout = "portraitCutout";
+// DBus Signal
+static const QString s_EnhanceFinishSignal = "finishedEnhance";
+
+AIModelServiceData::AIModelServiceData(AIModelService *q)
+ : qptr(q)
+{
+ supportNameToModel = initDBusModelList();
+ qInfo() << qPrintable("Support image enhance models:") << supportNameToModel;
+
+ if (!enhanceTemp.isValid()) {
+ qWarning() << qPrintable("Create enhance temp dir failed") << enhanceTemp.errorString();
+ } else {
+ qInfo() << qPrintable("Enhance temp dir:") << enhanceTemp.path();
+ }
+
+ if (!convertTemp.isValid()) {
+ qWarning() << qPrintable("Create convert temp dir failed") << convertTemp.errorString();
+ } else {
+ qInfo() << qPrintable("Convert temp dir:") << convertTemp.path();
+ }
+}
+
+/**
+ @return 返回当前存在的图像增强模型列表,通过读取DBu接口属性 modelList 取得,仅初始化调用一次
+ */
+QList> AIModelServiceData::initDBusModelList()
+{
+ // 预期的模型项顺序
+ QStringList sortModelList = {s_ModelColoring,
+ s_ModelSuperResol,
+ s_ModelBlurBkg,
+ s_ModelBkgCut,
+ s_ModelHand,
+ s_ModelCartoon2D,
+ s_ModelCartoon3D,
+ s_ModelSketch};
+
+ // 调用DBus接口获取模型列表
+ QDBusInterface interface(s_EnhanceService, s_EnhancePath, s_EnhanceInterface, QDBusConnection::systemBus());
+ QStringList modelList = interface.property("modelList").toStringList();
+ if (modelList.isEmpty()) {
+ auto error = interface.lastError();
+ qWarning() << QString("[Enhance DBus] Get model list failed, %1: %2").arg(error.name()).arg(error.message());
+ return {};
+ }
+
+ // 调整模型顺序, 模型-名称排序列表
+ QList> mapModelList;
+ for (const QString &model : sortModelList) {
+ if (modelList.contains(model)) {
+ modelList.removeOne(model);
+
+ auto ptr = createModelInfo(model);
+ mapModelInfo.insert(ptr->modelID, ptr);
+ mapModelList.append(qMakePair(ptr->modelID, ptr->modelTr));
+ }
+ }
+
+ // 不在名单的模型追加到末尾
+ for (const QString &appendModel : modelList) {
+ auto ptr = createModelInfo(appendModel);
+ mapModelInfo.insert(ptr->modelID, ptr);
+ mapModelList.append(qMakePair(ptr->modelID, ptr->modelTr));
+ }
+
+ return mapModelList;
+}
+
+/**
+ @brief 根据传入的模型名称创建模型信息,包含模型ID和翻译名称等。
+ */
+ModelPtr AIModelServiceData::createModelInfo(const QString &model)
+{
+ // 记录默认的模型-名称映射
+ static QMap mapNameModel = {
+ {s_ModelColoring, Coloring},
+ {s_ModelSuperResol, SuperResol},
+ {s_ModelBlurBkg, BackgroundBlur},
+ {s_ModelBkgCut, BackgroundCut},
+ {s_ModelHand, Hand},
+ {s_ModelCartoon2D, Cartoon2D},
+ {s_ModelCartoon3D, Cartoon3D},
+ {s_ModelSketch, Sketch},
+ };
+
+ // 用于国际化翻译
+ static QMap mapModelTrName = {
+ {Coloring, QObject::tr("Colorize pictures")},
+ {SuperResol, QObject::tr("Upgrade resolution")},
+ {BackgroundBlur, QObject::tr("Blurred background")},
+ {BackgroundCut, QObject::tr("Delete background")},
+ {Hand, QObject::tr("Hand-drawn cartoons")},
+ {Cartoon2D, QObject::tr("2D Manga")},
+ {Cartoon3D, QObject::tr("3D Manga")},
+ {Sketch, QObject::tr("Sketch")},
+ };
+
+ ModelPtr ptr(new ModelInfo);
+ ptr->model = model;
+
+ if (mapNameModel.contains(model)) {
+ ptr->modelID = mapNameModel.value(model);
+ ptr->modelTr = mapModelTrName.value(ptr->modelID);
+ } else {
+ ptr->modelTr = QObject::tr(model.toUtf8().data());
+ ptr->modelID = UserType + userModelCount;
+ userModelCount++;
+ }
+
+ return ptr;
+}
+
+/**
+ @brief 创建用户重试的提示信息,
+ */
+DFloatingMessage *AIModelServiceData::createReloadMessage(const QString &output)
+{
+ DFloatingMessage *msg = new DFloatingMessage(DFloatingMessage::ResidentType);
+ msg->setIcon(QIcon(":/common/error.svg"));
+ msg->setMessage(QObject::tr("Processing failure."));
+
+ QPushButton *reload = new QPushButton(QObject::tr("Retry"));
+ msg->setWidget(reload);
+
+ QObject::connect(qptr, &AIModelService::clearPreviousEnhance, msg, &DFloatingMessage::close);
+ QObject::connect(reload, &QPushButton::clicked, qptr, [=]() {
+ msg->close();
+ qptr->reloadImageProcessing(output);
+ });
+
+ return msg;
+}
+
+void AIModelServiceData::startDBusTimer()
+{
+ if (!dbusTimer.isActive()) {
+ dbusTimer.start(s_DBusTimeout, qptr);
+ }
+}
+
+void AIModelServiceData::stopDBusTimer()
+{
+ dbusTimer.stop();
+}
+
+AIModelService::AIModelService(QObject *parent)
+ : QObject(parent)
+ , dptr(new AIModelServiceData(this))
+{
+ connect(&dptr->enhanceWatcher, &QFutureWatcherBase::finished, this, [this]() {
+ EnhancePtr ptr = dptr->enhanceWatcher.result();
+ auto curState = static_cast(ptr->state.loadAcquire());
+
+ if (ptr && AIModelService::LoadFailed == curState) {
+ Q_EMIT enhanceEnd(ptr->source, ptr->output, curState);
+ } else {
+ // Note: 备用的超时机制
+ // 正常发送消息,等待消息结束
+ // dptr->startDBusTimer();
+ }
+ });
+
+ // 绑定DBus信号
+ bool conn = QDBusConnection::systemBus().connect(s_EnhanceService,
+ s_EnhancePath,
+ s_EnhanceInterface,
+ s_EnhanceFinishSignal,
+ this,
+ SLOT(onDBusEnhanceEnd(const QString &, int)));
+ if (!conn) {
+ qWarning()
+ << QString("[Enhance DBus] Connect dbus %1 signal %2 failed").arg(s_EnhanceInterface).arg(s_EnhanceFinishSignal);
+ }
+}
+
+AIModelService::~AIModelService() {}
+
+AIModelService *AIModelService::instance()
+{
+ static AIModelService ins;
+ return &ins;
+}
+
+/**
+ @return 返回是否允许使用AI模型,目前仅在安装模型后启用。
+ */
+bool AIModelService::isValid() const
+{
+ return !dptr->supportNameToModel.isEmpty();
+}
+
+/**
+ @brief 使用 \a type 类型模型执行图像增强处理 \a filePath,此函数会立即返回。
+ 当文件类型非 png 时,将根据传入的图片 \a image 信息转存图片为png格式。
+ 图像增强结果通过 enhanceEnd() 抛出。
+
+ 当前图像处理流程为:
+ * 图像传入,记录原数据和转换类型
+ * 数据传入子线程,主要用于将原始文件数据转换为PNG文件,耗时不定,移入子线程处理
+ * 子线程中调用DBus接口图像增强处理
+ * DBus接口调用失败,标记处理失败,子线程结束后抛出执行失败信号 enhanceEnd()
+ * DBus接口调用成功,等待DBus处理完成信号 onDBusEnhanceEnd()
+ * 在完成槽函数中调用处理完成信号 enhanceEnd()
+
+ @sa enhanceEnd, onDBusEnhanceEnd
+ */
+QString AIModelService::imageProcessing(const QString &filePath, int modelID, const QImage &image)
+{
+ if (!dptr->mapModelInfo.contains(modelID)) {
+ return {};
+ }
+
+ resetProcess();
+
+ // 如果图片已是增强后的图片,则获取源图片进行处理
+ QString sourceFile = sourceFilePath(filePath);
+
+ // 生命周期交由子线程维护
+ QImage caputureImage = image.copy();
+ dptr->lastOutput = dptr->enhanceTemp.filePath(QString("%1.png").arg(dptr->enhanceCache.size()));
+ QString model = dptr->mapModelInfo.value(modelID)->model;
+
+ EnhancePtr ptr(new EnhanceInfo(sourceFile, dptr->lastOutput, model));
+ ptr->index = dptr->enhanceCache.size();
+ ptr->state = Loading;
+ dptr->enhanceCache.insert(ptr->output, ptr);
+
+ qInfo() << QString("Call enhance processing %1, %2").arg(dptr->lastOutput).arg(modelID);
+
+ QFuture f = QtConcurrent::run([=]() -> EnhancePtr {
+ // 写入文件移动到子线程。
+ QString tmpSrcFile = checkConvertFile(sourceFile, caputureImage);
+ if (tmpSrcFile.isEmpty()) {
+ tmpSrcFile = ptr->source;
+ }
+
+ // 若DBus调用失败,则直接返回错误
+ bool ret = AIModelServiceData::sendImageEnhance(tmpSrcFile, ptr->output, ptr->model);
+ if (!ret) {
+ ptr->state.store(LoadFailed);
+ }
+
+ return ptr;
+ });
+ dptr->enhanceWatcher.setFuture(f);
+
+ Q_EMIT enhanceStart();
+ return dptr->lastOutput;
+}
+
+/**
+ @brief 重新尝试之前的模型调用,仅允许重复最后一次调用
+ */
+void AIModelService::reloadImageProcessing(const QString &filePath)
+{
+ // 仅允许最后一次调用
+ EnhancePtr ptr = dptr->enhanceCache.value(filePath);
+ if (ptr.isNull() || ptr->index != dptr->enhanceCache.size() - 1) {
+ return;
+ }
+
+ resetProcess();
+
+ // 如果图片已是增强后的图片,则获取源图片进行处理
+ QString sourceFile = sourceFilePath(filePath);
+ ptr->state.storeRelease(Loading);
+ qInfo() << QString("Reload enhance processing %1, %2").arg(ptr->output).arg(ptr->model);
+
+ QFuture f = QtConcurrent::run([=]() -> EnhancePtr {
+ // 已处理过的数据,一般存在缓存
+ QString tmpSrcFile = checkConvertFile(sourceFile, QImage());
+ if (tmpSrcFile.isEmpty()) {
+ tmpSrcFile = ptr->source;
+ }
+
+ // 若 DBus 调用失败,则直接返回错误
+ bool ret = AIModelServiceData::sendImageEnhance(tmpSrcFile, ptr->output, ptr->model);
+ if (!ret) {
+ ptr->state.store(LoadFailed);
+ }
+
+ return ptr;
+ });
+ dptr->enhanceWatcher.setFuture(f);
+
+ Q_EMIT enhanceReload(filePath);
+}
+
+/**
+ @return 返回 \a filePath 是否为图像增强后的临时文件
+ */
+bool AIModelService::isTemporaryFile(const QString &filePath)
+{
+ return dptr->enhanceCache.contains(filePath);
+}
+
+/**
+ @return 当前提供的图像增强模型
+ */
+QList> AIModelService::supportModel() const
+{
+ return dptr->supportNameToModel;
+}
+
+/**
+ @brief 判断传入模型是否允许使用,将根据图片信息进行判断
+ 1. 仅允许静态图
+ 2. 其他模型: (可转换方向)不允许像素大小超过 2160x1440
+ 超分辨率模型: 不允许像素大小 512x512
+ */
+AIModelService::Error AIModelService::modelEnabled(int modelID, const QString &filePath) const
+{
+ auto info = LibCommonService::instance()->getImgInfoByPath(filePath);
+ if (imageViewerSpace::ImageTypeStatic != info.imageType) {
+ return FormatError;
+ }
+
+ switch (modelID) {
+ // 待定需求,模糊背景和删除背景不设置限制
+#if 0
+ case AIModelServiceData::BackgroundBlur:
+ Q_FALLTHROUGH();
+ case AIModelServiceData::BackgroundCut:
+ return NoError;
+#endif
+ case AIModelServiceData::SuperResol: {
+ const int resolLimitWidth = 512;
+ const int resolLimitHeight = 512;
+
+ if ((resolLimitWidth < info.imgOriginalWidth) || (resolLimitHeight < info.imgOriginalHeight)) {
+ return PixelSizeError;
+ }
+ break;
+ }
+ default: {
+ const int normalLimitWidth = 2160;
+ const int normalLimitHeight = 1440;
+
+ if (info.imgOriginalHeight < info.imgOriginalWidth) {
+ if ((normalLimitWidth < info.imgOriginalWidth) || (normalLimitHeight < info.imgOriginalHeight)) {
+ return PixelSizeError;
+ }
+ } else {
+ // 高比宽大,旋转方向
+ if ((normalLimitWidth < info.imgOriginalHeight) || (normalLimitHeight < info.imgOriginalWidth)) {
+ return PixelSizeError;
+ }
+ }
+ break;
+ }
+ }
+
+ return NoError;
+}
+
+/**
+ @return 若为图像增强文件,返回 \a filePath 指向的源文件,否则返回自身
+ */
+QString AIModelService::sourceFilePath(const QString &filePath)
+{
+ if (dptr->enhanceCache.contains(filePath)) {
+ auto ptr = dptr->enhanceCache.value(filePath);
+ return ptr->source;
+ }
+ return filePath;
+}
+
+/**
+ @return 返回是否正在等待保存,若之前还未保存,不会重复弹窗
+ */
+bool AIModelService::isWaitSave() const
+{
+ return dptr->waitSave;
+}
+
+/**
+ @brief 弹出对话框提示是否保存当前文件 \a filePath
+ */
+void AIModelService::saveFileDialog(const QString &filePath)
+{
+ if (isWaitSave()) {
+ return;
+ }
+ dptr->waitSave = true;
+
+ Dtk::Widget::DDialog expiredDialog;
+ expiredDialog.setIcon(QIcon::fromTheme("deepin-image-viewer"));
+ expiredDialog.setMessage(tr("Image not saved, Do you want to save it?"));
+
+ expiredDialog.addButton(tr("Cancel"), false, Dtk::Widget::DDialog::ButtonNormal);
+ const int suggestRet = expiredDialog.addButton(tr("Save as"), true, Dtk::Widget::DDialog::ButtonRecommend);
+
+ int ret = expiredDialog.exec();
+ if (ret == suggestRet) {
+ saveEnhanceFileAs(filePath);
+ }
+
+ dptr->waitSave = false;
+}
+
+/**
+ @brief 保存增强后的图像 \a filePath , 如果非图像增强文件则不进行保存
+ */
+void AIModelService::saveEnhanceFile(const QString &filePath)
+{
+ if (!isTemporaryFile(filePath)) {
+ return;
+ }
+
+ // 覆盖原文件
+ saveFile(filePath, sourceFilePath(filePath));
+}
+
+/**
+ @brief 另存增强后的图像 \a filePath , 如果非图像增强文件则不进行保存
+ */
+void AIModelService::saveEnhanceFileAs(const QString &filePath)
+{
+ if (!isTemporaryFile(filePath)) {
+ return;
+ }
+
+ saveTemporaryAs(filePath, sourceFilePath(filePath));
+}
+
+/**
+ @brief 复位进行中的图像处理,实际run调用的处理不会立即关闭,主要用于标记状态。
+ 同时发送信号清理之前的增强信息,例如关闭含有"重试"的浮动信息
+ */
+void AIModelService::resetProcess()
+{
+ if (dptr->enhanceWatcher.isRunning()) {
+ dptr->enhanceWatcher.cancel();
+ }
+ // 清理之前的图像增强状态
+ Q_EMIT clearPreviousEnhance();
+}
+
+/**
+ @brief 判断传入错误类型 \a error 并使用浮动提示窗提示。
+ 若错误通过这种方式提示,则返回 true
+ */
+bool AIModelService::detectErrorAndNotify(QWidget *targetWidget, AIModelService::Error error, const QString &output)
+{
+ bool detectError = true;
+ switch (error) {
+ case FormatError:
+ DMessageManager::instance()->sendMessage(
+ targetWidget, QIcon(":/common/error.svg"), tr("Image format is not supported, please switch the image."));
+ break;
+ case PixelSizeError:
+ DMessageManager::instance()->sendMessage(targetWidget,
+ QIcon(":/common/error.svg"),
+ tr("The image resolution exceeds the limit, please switch the image."));
+ break;
+ case LoadFiledError:
+ DMessageManager::instance()->sendMessage(targetWidget, dptr->createReloadMessage(output));
+ break;
+ case NotDetectFaceError:
+ DMessageManager::instance()->sendMessage(
+ targetWidget, QIcon(":/common/error.svg"), tr("Portrait not detected, switch pictures."));
+ break;
+ default:
+ detectError = false;
+ break;
+ }
+
+ return detectError;
+}
+
+/**
+ @return 返回文件 \a filePath 的图像增强状态,是否处于运行中。
+ */
+AIModelService::State AIModelService::enhanceState(const QString &filePath)
+{
+ if (isValid() && dptr->enhanceCache.contains(filePath)) {
+ auto ptr = dptr->enhanceCache.value(filePath);
+ return static_cast(ptr->state.loadAcquire());
+ }
+
+ return None;
+}
+
+/**
+ @brief 应用判断超时处理
+ */
+void AIModelService::timerEvent(QTimerEvent *e)
+{
+ if (e->timerId() == dptr->dbusTimer.timerId()) {
+ // 触发超时
+ dptr->stopDBusTimer();
+ // 获取最近的输出图片
+ onDBusEnhanceEnd(dptr->lastOutput, AIModelServiceData::DBusFailed);
+ }
+
+ QObject::timerEvent(e);
+}
+
+/**
+ @brief 保存文件 \a filePath 到 \a newPath , 默认覆盖 \a newPath 存在文件时无效,
+ 需要先移除旧文件。
+ */
+bool AIModelService::saveFile(const QString &filePath, const QString &newPath)
+{
+ // QFile::copy() will not overwrite.
+ if (QFile::exists(newPath)) {
+ QFile file(newPath);
+ if (!file.remove()) {
+ qWarning() << QString("Remove previous file failed! %1").arg(file.errorString());
+ return false;
+ }
+ }
+
+ bool ret = QFile::copy(filePath, newPath);
+ if (!ret) {
+ qWarning() << QString("Copy temporary file %1 failed").arg(filePath);
+ }
+
+ return ret;
+}
+
+/**
+ @brief 弹出保存框将 \a filePath 保存,建议目录为源文件目录 \a sourcePath
+ */
+void AIModelService::saveTemporaryAs(const QString &filePath, const QString &sourcePath)
+{
+ QFileInfo info(sourcePath);
+ QString dir = info.absolutePath();
+ if (dir.isEmpty()) {
+ dir = QDir::homePath();
+ }
+
+ Dtk::Widget::DFileDialog dialog(nullptr, tr("Save"));
+ dialog.setAcceptMode(QFileDialog::AcceptSave);
+ dialog.setDirectory(dir);
+ dialog.selectFile(info.completeBaseName());
+ dialog.setNameFilter("*.png");
+
+ int mode = dialog.exec();
+ if (QDialog::Accepted == mode) {
+ auto files = dialog.selectedFiles();
+ if (files.isEmpty()) {
+ return;
+ }
+
+ QString newPath = files.value(0);
+ saveFile(filePath, newPath);
+ }
+}
+
+/**
+ @brief 判断模型是否支持源文件 \a filePath, 若为不支持类型,转换为 png 格式图片。
+ 需考虑图片变更的场景。
+ */
+QString AIModelService::checkConvertFile(const QString &filePath, const QImage &image) const
+{
+ // WARNING: 判断图片变更
+ QMutexLocker _locker(&dptr->cacheMutex);
+ if (dptr->convertCache.contains(filePath)) {
+ return dptr->convertCache.value(filePath);
+ }
+
+ if (image.isNull()) {
+ return {};
+ }
+
+ QString cvtFile;
+ cvtFile = dptr->convertTemp.filePath(QString("%1_%2.png").arg(dptr->convertCache.size()).arg(QFileInfo(filePath).fileName()));
+
+ _locker.unlock();
+ if (!image.save(cvtFile, "PNG")) {
+ return {};
+ }
+
+ _locker.relock();
+ dptr->convertCache.insert(filePath, cvtFile);
+ return cvtFile;
+}
+
+/**
+ @brief 发送DBus图像增强处理消息。
+ 删除背景,模糊背景使用单独接口。
+ */
+bool AIModelServiceData::sendImageEnhance(const QString &source, const QString &output, const QString &model)
+{
+ // 此接口调用后程序停止,因此不适用 QDBusInterface::isValid() 直接调用 call() 会唤醒 DBus 服务
+ QDBusInterface interface(s_EnhanceService, s_EnhancePath, s_EnhanceInterface, QDBusConnection::systemBus());
+
+ QDBusMessage message;
+ // 删除背景,模糊背景单独处理
+ QString procMethod;
+ if (s_ModelBlurBkg == model) {
+ procMethod = s_EnhanceBlurBkg;
+ message = interface.call(s_EnhanceBlurBkg, source, output);
+ } else if (s_ModelBkgCut == model) {
+ procMethod = s_EnhancePortraitCout;
+ message = interface.call(s_EnhancePortraitCout, source, output);
+ } else {
+ procMethod = s_EnhanceProcMethod;
+ message = interface.call(s_EnhanceProcMethod, source, output, model);
+ }
+
+ QDBusError error = interface.lastError();
+ if (QDBusError::NoError != error.type()) {
+ qWarning() << QString("[Enhance DBus] DBus %1 call %2 error: type(%2) [%3] %4")
+ .arg(s_EnhanceService)
+ .arg(procMethod)
+ .arg(error.type())
+ .arg(error.name())
+ .arg(error.message());
+
+ } else {
+ QDBusReply reply(message);
+ bool ret = reply.value().toBool();
+
+ if (!ret) {
+ qWarning() << QString("[Enhance DBus] Call %1 error: value(%2)").arg(s_EnhanceProcMethod).arg(ret);
+ }
+ return ret;
+ }
+
+ return false;
+}
+
+/**
+ @brief 接收DBus接口处理完成的信号,\a output 是输出的文件路径。
+ */
+void AIModelService::onDBusEnhanceEnd(const QString &output, int error)
+{
+ // 多实例,可能传入其它实例的任务
+ EnhancePtr ptr = dptr->enhanceCache.value(output);
+ if (ptr.isNull()) {
+ return;
+ }
+
+ // 只允许最新的图片更新
+ if (ptr->index != dptr->enhanceCache.size() - 1) {
+ return;
+ }
+
+ State state = static_cast(ptr->state.loadAcquire());
+ if (LoadFailed == state) {
+ qWarning() << qPrintable("[Enhance DBus] Reentrant enhance image process! ") << output;
+ }
+
+ // 判断接口反馈错误
+ switch (error) {
+ case AIModelServiceData::DBusNoError: {
+ // 判断文件是否创建成功
+ if (!QFile::exists(output)) {
+ qWarning() << qPrintable("[Enhance DBus] Create enhance image failed! ") << output;
+ state = LoadFailed;
+ } else {
+ state = LoadSucc;
+ }
+ break;
+ }
+ case AIModelServiceData::DBusNotDetect:
+ state = NotDetectFace;
+ break;
+ default:
+ state = LoadFailed;
+ break;
+ }
+
+ ptr->state.storeRelease(state);
+
+ Q_EMIT enhanceEnd(ptr->source, output, state);
+}
diff --git a/libimageviewer/service/aimodelservice.h b/libimageviewer/service/aimodelservice.h
new file mode 100644
index 00000000..8a35b986
--- /dev/null
+++ b/libimageviewer/service/aimodelservice.h
@@ -0,0 +1,69 @@
+// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef AIMODELSERVICE_H
+#define AIMODELSERVICE_H
+
+#include
+#include
+#include
+
+class AIModelServiceData;
+class AIModelService : public QObject
+{
+ Q_OBJECT
+
+public:
+ static AIModelService *instance();
+
+ bool isValid() const;
+
+ // Image enahance
+ enum State { None, Loading, LoadSucc, LoadFailed, NotDetectFace, LoadTimeout };
+ State enhanceState(const QString &filePath);
+
+ QList> supportModel() const;
+ enum Error { NoError, FormatError, PixelSizeError, LoadFiledError, NotDetectFaceError };
+ Error modelEnabled(int modelID, const QString &filePath) const;
+
+ QString imageProcessing(const QString &filePath, int modelID, const QImage &image);
+ Q_SLOT void reloadImageProcessing(const QString &filePath);
+
+ bool isTemporaryFile(const QString &filePath);
+ QString sourceFilePath(const QString &filePath);
+
+ bool isWaitSave() const;
+ void saveFileDialog(const QString &filePath);
+ void saveEnhanceFile(const QString &filePath);
+ void saveEnhanceFileAs(const QString &filePath);
+ void resetProcess();
+
+ bool detectErrorAndNotify(QWidget *targetWidget, Error error, const QString &output = QString::null);
+
+ Q_SIGNAL void enhanceStart();
+ Q_SIGNAL void enhanceReload(const QString &output);
+ Q_SIGNAL void enhanceEnd(const QString &source, const QString &output, State state);
+
+ // 图片切换等操作时,清理之前的图像增强状态
+ Q_SIGNAL void clearPreviousEnhance();
+
+protected:
+ void timerEvent(QTimerEvent *e) override;
+
+private:
+ AIModelService(QObject *parent = nullptr);
+ ~AIModelService() override;
+
+ bool saveFile(const QString &filePath, const QString &newPath);
+ void saveTemporaryAs(const QString &filePath, const QString &sourcePath);
+ QString checkConvertFile(const QString &filePath, const QImage &image) const;
+
+ // DBus
+ Q_SLOT void onDBusEnhanceEnd(const QString &output, int error);
+
+private:
+ QScopedPointer dptr;
+};
+
+#endif // AIMODELSERVICE_H
diff --git a/libimageviewer/service/aimodelservice_p.h b/libimageviewer/service/aimodelservice_p.h
new file mode 100644
index 00000000..7bfe6fb4
--- /dev/null
+++ b/libimageviewer/service/aimodelservice_p.h
@@ -0,0 +1,105 @@
+// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef AIMODELSERVICE_P_H
+#define AIMODELSERVICE_P_H
+
+#include "aimodelservice.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+DWIDGET_USE_NAMESPACE
+
+// 模型信息
+struct ModelInfo
+{
+ int modelID; // 模型ID AIModelServiceData::EnhanceType
+ QString model; // 模型名称
+ QString modelTr; // 模型翻译名称
+};
+
+typedef QSharedPointer ModelPtr;
+
+// 图像增强处理信息
+struct EnhanceInfo
+{
+ const QString source;
+ const QString output;
+ const QString model;
+ int index = 0;
+
+ QAtomicInt state = AIModelService::None; // 处理状态,可能有争用
+
+ EnhanceInfo(const QString &s, const QString &o, const QString &m)
+ : source(s)
+ , output(o)
+ , model(m)
+ {
+ }
+};
+
+typedef QSharedPointer EnhancePtr;
+
+class AIModelServiceData
+{
+public:
+ // 图像增强服务类型
+ enum EnhanceType {
+ Coloring,
+ SuperResol, // 超分辨率
+ BackgroundBlur, // 模糊背景
+ BackgroundCut, // 删除背景(人像抠图)
+ Hand,
+ Cartoon2D,
+ Cartoon3D,
+ Sketch,
+ UserType = 1024, // 用户类型,后续可能增强其它模型
+ };
+
+ // DBus接口反馈错误
+ enum DBusError {
+ DBusNoError = 0,
+ DBusFailed = -1, // 执行失败
+ DBusNotDetect = -2, // 未检测人像
+ };
+
+ AIModelServiceData(AIModelService *q);
+ QList> initDBusModelList();
+ ModelPtr createModelInfo(const QString &model);
+
+ DFloatingMessage *createReloadMessage(const QString &output);
+ static bool sendImageEnhance(const QString &source, const QString &output, const QString &model);
+
+ void startDBusTimer();
+ void stopDBusTimer();
+
+ AIModelService *qptr = nullptr;
+ int userModelCount = 0; // 非默认的模型统计
+ QMap mapModelInfo;
+ QList> supportNameToModel; // 缓存的支持模型列表<模型ID,名称>
+
+ QString lastOutput; // 最近的图像增强输出文件
+ QTemporaryDir enhanceTemp; // 图像增强文件临时目录
+ QHash enhanceCache; // 图像增强缓存信息(仅主线程访问)
+
+ QMutex cacheMutex;
+ QTemporaryDir convertTemp; // 图像类型转换文件临时目录
+ QHash convertCache; // 缓存的信息,可能多个线程访问
+
+ QFutureWatcher enhanceWatcher;
+
+ bool waitSave = false; // 是否在等待保存操作结束
+ QBasicTimer dbusTimer; // DBus处理超时定时器
+};
+
+#endif // AIMODELSERVICE_P_H
diff --git a/libimageviewer/translations/libimageviewer.ts b/libimageviewer/translations/libimageviewer.ts
index b1e7b2ff..bccd4dca 100644
--- a/libimageviewer/translations/libimageviewer.ts
+++ b/libimageviewer/translations/libimageviewer.ts
@@ -1,6 +1,24 @@
+
+ AIEnhanceFloatButton
+
+
+
+
+
+
+
+
+
+
+ AIModelService
+
+
+
+
+
ExtensionPanel
@@ -57,6 +75,18 @@
Favorite
+
+
+
+
+
+
+
+
+
+
+
+
LockWidget
@@ -236,12 +266,36 @@
- day
+ day
Photo info
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
RenameDialog
@@ -285,6 +339,29 @@
Exit
+
+ TextToImageDialog
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
ThumbnailWidget
diff --git a/libimageviewer/translations/libimageviewer_zh_CN.ts b/libimageviewer/translations/libimageviewer_zh_CN.ts
index dae470f3..d96db8c9 100644
--- a/libimageviewer/translations/libimageviewer_zh_CN.ts
+++ b/libimageviewer/translations/libimageviewer_zh_CN.ts
@@ -1,6 +1,52 @@
+
+ AIEnhanceFloatWidget
+
+
+ 保存
+
+
+
+ 另存为
+
+
+
+ 重置
+
+
+
+ AIModelService
+
+
+ 保存
+
+
+
+ 另存为
+
+
+
+ 取 消
+
+
+
+ 图片未保存,是否保存?
+
+
+
+ 图像格式不支持,请切换图片。
+
+
+
+ 图像分辨率超过限制,请切换图片。
+
+
+
+ 未检测到人像,请切换图片。
+
+
ExtensionPanel
@@ -12,6 +58,13 @@
Ctrl+I
+
+ LibImageGraphicsView
+
+
+ AI修图中,请稍等...
+
+
LibImageInfoWidget
@@ -57,6 +110,18 @@
收藏
+
+
+ 还原
+
+
+
+ 保存
+
+
+
+ AI修图
+
LockWidget
@@ -236,12 +301,52 @@
- 天
+ 天
照片信息
+
+
+ 重 试
+
+
+
+ 图片上色
+
+
+
+ 提升分辨率
+
+
+
+ 模糊背景
+
+
+
+ 删除背景
+
+
+
+ 手绘漫画
+
+
+
+ 2D漫画
+
+
+
+ 素描
+
+
+
+ 处理失败。
+
+
+
+ 3D漫画
+
RenameDialog
diff --git a/libimageviewer/translations/libimageviewer_zh_HK.ts b/libimageviewer/translations/libimageviewer_zh_HK.ts
index f324d78d..50a230ea 100644
--- a/libimageviewer/translations/libimageviewer_zh_HK.ts
+++ b/libimageviewer/translations/libimageviewer_zh_HK.ts
@@ -1,6 +1,63 @@
+
+ AIEnhanceFloatButton
+
+
+ 保存
+
+
+
+ 還原
+
+
+
+ AIEnhanceFloatWidget
+
+
+ 保存
+
+
+
+ 另存爲
+
+
+
+ 還原
+
+
+
+ AIModelService
+
+
+ 保存
+
+
+
+ 另存爲
+
+
+
+ 取 消
+
+
+
+ 圖片未保存,是否保存?
+
+
+
+ 圖像格式不支持,請切換圖片。
+
+
+
+ 圖像分辨率超過限制,請切換圖片。
+
+
+
+ 未檢測到人像,請切換圖片。
+
+
ExtensionPanel
@@ -12,6 +69,13 @@
Ctrl+I
+
+ LibImageGraphicsView
+
+
+ AI修圖中,請稍等...
+
+
LibImageInfoWidget
@@ -57,6 +121,18 @@
收藏
+
+
+ 還原
+
+
+
+ 保存
+
+
+
+ AI修圖
+
LockWidget
@@ -236,12 +312,52 @@
- 天
+ 天
照片信息
+
+
+ 重 試
+
+
+
+ 圖片上色
+
+
+
+ 提升分辨率
+
+
+
+ 模糊背景
+
+
+
+ 刪除背景
+
+
+
+ 手繪漫畫
+
+
+
+ 2D漫畫
+
+
+
+ 素描
+
+
+
+ 處理失敗。
+
+
+
+ 3D漫畫
+
RenameDialog
diff --git a/libimageviewer/translations/libimageviewer_zh_TW.ts b/libimageviewer/translations/libimageviewer_zh_TW.ts
index 5f6bca4d..ec5e3aa1 100644
--- a/libimageviewer/translations/libimageviewer_zh_TW.ts
+++ b/libimageviewer/translations/libimageviewer_zh_TW.ts
@@ -1,6 +1,63 @@
+
+ AIEnhanceFloatButton
+
+
+ 保存
+
+
+
+ 还原
+
+
+
+ AIEnhanceFloatWidget
+
+
+ 保存
+
+
+
+ 另存為
+
+
+
+ 還原
+
+
+
+ AIModelService
+
+
+ 保存
+
+
+
+ 另存為
+
+
+
+ 取 消
+
+
+
+ 圖片未保存,是否保存?
+
+
+
+ 圖像格式不支持,請切換圖片。
+
+
+
+ 圖像分辨率超過限制,請切換圖片。
+
+
+
+ 未檢測到人像,請切換圖片。
+
+
ExtensionPanel
@@ -12,6 +69,13 @@
Ctrl+I
+
+ LibImageGraphicsView
+
+
+ AI修圖中,請稍等...
+
+
LibImageInfoWidget
@@ -57,6 +121,18 @@
收藏
+
+
+ 還原
+
+
+
+ 保存
+
+
+
+ AI修圖
+
LockWidget
@@ -236,12 +312,52 @@
- 天
+ 天
照片訊息
+
+
+ 重 試
+
+
+
+ 圖片上色
+
+
+
+ 提升分辨率
+
+
+
+ 模糊背景
+
+
+
+ 刪除背景
+
+
+
+ 手繪漫畫
+
+
+
+ 2D漫畫
+
+
+
+ 素描
+
+
+
+ 處理失敗。
+
+
+
+ 3D漫畫
+
RenameDialog
diff --git a/libimageviewer/viewpanel/contents/aienhancefloatwidget.cpp b/libimageviewer/viewpanel/contents/aienhancefloatwidget.cpp
new file mode 100644
index 00000000..eac5781f
--- /dev/null
+++ b/libimageviewer/viewpanel/contents/aienhancefloatwidget.cpp
@@ -0,0 +1,69 @@
+// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "aienhancefloatwidget.h"
+
+#include
+
+#include
+
+const int FLOAT_WDITH = 72;
+const int FLOAT_HEIGHT = 172;
+const int FLOAT_RIGHT_MARGIN = 15;
+const int FLOAT_RADIUS = 18;
+const QSize FLOAT_BTN_SIZE = QSize(40, 40);
+const QSize FLOAT_ICON_SIZE = QSize(20, 20);
+
+AIEnhanceFloatWidget::AIEnhanceFloatWidget(QWidget *parent)
+ : DFloatingWidget(parent)
+{
+ setObjectName("AIEnhanceFloatWidget");
+ setFixedSize(FLOAT_WDITH, FLOAT_HEIGHT);
+ setFramRadius(FLOAT_RADIUS);
+ setBlurBackgroundEnabled(true);
+ initButtton();
+
+ if (parent) {
+ DAnchorsBase::setAnchor(this, Qt::AnchorRight, parent, Qt::AnchorRight);
+ DAnchorsBase::setAnchor(this, Qt::AnchorVerticalCenter, parent, Qt::AnchorVerticalCenter);
+ DAnchorsBase *anchor = DAnchorsBase::getAnchorBaseByWidget(this);
+ if (anchor) {
+ anchor->setRightMargin(FLOAT_RIGHT_MARGIN);
+ }
+ }
+}
+
+void AIEnhanceFloatWidget::initButtton()
+{
+ QVBoxLayout *mainLayout = new QVBoxLayout;
+ mainLayout->setAlignment(Qt::AlignCenter);
+ mainLayout->setSpacing(10);
+
+ resetBtn = new DIconButton(this);
+ resetBtn->setFixedSize(FLOAT_BTN_SIZE);
+ resetBtn->setIcon(QIcon::fromTheme("dcc_reset"));
+ resetBtn->setIconSize(FLOAT_ICON_SIZE);
+ resetBtn->setToolTip(tr("Reprovision"));
+ mainLayout->addWidget(resetBtn);
+
+ saveBtn = new DIconButton(this);
+ saveBtn->setFixedSize(FLOAT_BTN_SIZE);
+ saveBtn->setIcon(QIcon::fromTheme("dcc_save"));
+ saveBtn->setIconSize(FLOAT_ICON_SIZE);
+ saveBtn->setToolTip(tr("Save"));
+ mainLayout->addWidget(saveBtn);
+
+ saveAsBtn = new DIconButton(this);
+ saveAsBtn->setFixedSize(FLOAT_BTN_SIZE);
+ saveAsBtn->setIcon(QIcon::fromTheme("dcc_file_save_as"));
+ saveAsBtn->setIconSize(FLOAT_ICON_SIZE);
+ saveAsBtn->setToolTip(tr("Save as"));
+ mainLayout->addWidget(saveAsBtn);
+
+ setLayout(mainLayout);
+
+ connect(resetBtn, &DIconButton::clicked, this, &AIEnhanceFloatWidget::reset);
+ connect(saveBtn, &DIconButton::clicked, this, &AIEnhanceFloatWidget::save);
+ connect(saveAsBtn, &DIconButton::clicked, this, &AIEnhanceFloatWidget::saveAs);
+}
diff --git a/libimageviewer/viewpanel/contents/aienhancefloatwidget.h b/libimageviewer/viewpanel/contents/aienhancefloatwidget.h
new file mode 100644
index 00000000..0283874f
--- /dev/null
+++ b/libimageviewer/viewpanel/contents/aienhancefloatwidget.h
@@ -0,0 +1,32 @@
+// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef AIENHANCEFLOATWIDGET_H
+#define AIENHANCEFLOATWIDGET_H
+
+#include
+#include
+
+DWIDGET_USE_NAMESPACE
+
+class AIEnhanceFloatWidget : public DFloatingWidget
+{
+ Q_OBJECT
+public:
+ explicit AIEnhanceFloatWidget(QWidget *parent = nullptr);
+
+ Q_SIGNAL void reset();
+ Q_SIGNAL void save();
+ Q_SIGNAL void saveAs();
+
+private:
+ void initButtton();
+
+private:
+ DIconButton *resetBtn = nullptr;
+ DIconButton *saveBtn = nullptr;
+ DIconButton *saveAsBtn = nullptr;
+};
+
+#endif // AIENHANCEFLOATWIDGET_H
diff --git a/libimageviewer/viewpanel/contents/imgviewwidget.cpp b/libimageviewer/viewpanel/contents/imgviewwidget.cpp
index 379273f5..c4358e0f 100644
--- a/libimageviewer/viewpanel/contents/imgviewwidget.cpp
+++ b/libimageviewer/viewpanel/contents/imgviewwidget.cpp
@@ -72,6 +72,9 @@ bool MyImageListWidget::eventFilter(QObject *obj, QEvent *e)
qDebug() << "QEvent::Leave" << obj;
}
if (e->type() == QEvent::MouseButtonPress) {
+ if (!isEnabled()) {
+ return true;
+ }
QMouseEvent *mouseEvent = dynamic_cast(e);
m_pressPoint = mouseEvent->globalPos();
@@ -88,6 +91,10 @@ bool MyImageListWidget::eventFilter(QObject *obj, QEvent *e)
qDebug() << "------------getCount = " << LibImageDataService::instance()->getCount();
}
if (e->type() == QEvent::MouseButtonRelease) {
+ if (!isEnabled()) {
+ return true;
+ }
+
if (m_movePoints.size() > 0) {
int endPos = m_movePoints.last().x() - m_movePoints.first().x();
//过滤掉触屏点击时的move误操作
@@ -101,6 +108,10 @@ bool MyImageListWidget::eventFilter(QObject *obj, QEvent *e)
// animationStart(true, 0, 400);
}
if (e->type() == QEvent::MouseMove || e->type() == QEvent::TouchUpdate) {
+ if (!isEnabled()) {
+ return true;
+ }
+
QMouseEvent *mouseEvent = dynamic_cast(e);
if (!mouseEvent) {
return false;
diff --git a/libimageviewer/viewpanel/scen/graphicsitem.cpp b/libimageviewer/viewpanel/scen/graphicsitem.cpp
index e9a3d019..1c939661 100644
--- a/libimageviewer/viewpanel/scen/graphicsitem.cpp
+++ b/libimageviewer/viewpanel/scen/graphicsitem.cpp
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2020 - 2022 UnionTech Software Technology Co., Ltd.
+// SPDX-FileCopyrightText: 2020 - 2023 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later
@@ -7,6 +7,10 @@
#include
#include
+#include
+
+DGUI_USE_NAMESPACE
+
LibGraphicsMovieItem::LibGraphicsMovieItem(const QString &fileName, const QString &suffix, QGraphicsItem *parent)
: QGraphicsPixmapItem(fileName, parent)
{
@@ -15,8 +19,9 @@ LibGraphicsMovieItem::LibGraphicsMovieItem(const QString &fileName, const QStrin
setTransformationMode(Qt::SmoothTransformation);
m_movie = new QMovie(fileName);
- QObject::connect(m_movie, &QMovie::frameChanged, this, [ = ] {
- if (m_movie.isNull()) return;
+ QObject::connect(m_movie, &QMovie::frameChanged, this, [=] {
+ if (m_movie.isNull())
+ return;
setPixmap(m_movie->currentPixmap());
});
//自动执行播放
@@ -57,11 +62,9 @@ void LibGraphicsMovieItem::stop()
m_movie->stop();
}
-
LibGraphicsPixmapItem::LibGraphicsPixmapItem(const QPixmap &pixmap)
: QGraphicsPixmapItem(pixmap, nullptr)
{
-
}
LibGraphicsPixmapItem::~LibGraphicsPixmapItem()
@@ -82,8 +85,7 @@ void LibGraphicsPixmapItem::paint(QPainter *painter, const QStyleOptionGraphicsI
if (ts.type() == QTransform::TxScale && ts.m11() < 1) {
QPixmap currentPixmap = pixmap();
if (currentPixmap.width() < 10000 && currentPixmap.height() < 10000) {
- painter->setRenderHint(QPainter::SmoothPixmapTransform,
- (transformationMode() == Qt::SmoothTransformation));
+ painter->setRenderHint(QPainter::SmoothPixmapTransform, (transformationMode() == Qt::SmoothTransformation));
Q_UNUSED(option);
Q_UNUSED(widget);
@@ -109,4 +111,34 @@ void LibGraphicsPixmapItem::paint(QPainter *painter, const QStyleOptionGraphicsI
}
}
+LibGraphicsMaskItem::LibGraphicsMaskItem(QGraphicsItem *parent)
+ : QGraphicsRectItem(parent)
+{
+ onThemeChange(DGuiApplicationHelper::instance()->themeType());
+ conn = QObject::connect(DGuiApplicationHelper::instance(),
+ &DGuiApplicationHelper::themeTypeChanged,
+ [this](DGuiApplicationHelper::ColorType themeType) { this->onThemeChange(themeType); });
+}
+
+LibGraphicsMaskItem::~LibGraphicsMaskItem()
+{
+ QObject::disconnect(conn);
+}
+void LibGraphicsMaskItem::onThemeChange(int theme)
+{
+ QColor maskColor;
+ if (DGuiApplicationHelper::ColorType::DarkType == theme) {
+ maskColor = QColor(Qt::black);
+ maskColor.setAlphaF(0.6);
+ } else {
+ maskColor = QColor(Qt::white);
+ maskColor.setAlphaF(0.6);
+ }
+
+ QPen curPen = pen();
+ curPen.setColor(maskColor);
+ setPen(curPen);
+ setBrush(maskColor);
+ update();
+}
diff --git a/libimageviewer/viewpanel/scen/graphicsitem.h b/libimageviewer/viewpanel/scen/graphicsitem.h
index 4ea63a22..c039bb87 100644
--- a/libimageviewer/viewpanel/scen/graphicsitem.h
+++ b/libimageviewer/viewpanel/scen/graphicsitem.h
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2020 - 2022 UnionTech Software Technology Co., Ltd.
+// SPDX-FileCopyrightText: 2020 - 2023 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later
@@ -29,11 +29,25 @@ class LibGraphicsPixmapItem : public QGraphicsPixmapItem
~LibGraphicsPixmapItem() override;
void setPixmap(const QPixmap &pixmap);
+
protected:
//自绘函数
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
+
private:
QPair cachePixmap;
};
-#endif // GRAPHICSMOVIEITEM_H
+class LibGraphicsMaskItem : public QGraphicsRectItem
+{
+public:
+ explicit LibGraphicsMaskItem(QGraphicsItem *parent = nullptr);
+ ~LibGraphicsMaskItem();
+
+ void onThemeChange(int theme);
+
+private:
+ QMetaObject::Connection conn;
+};
+
+#endif // GRAPHICSMOVIEITEM_H
diff --git a/libimageviewer/viewpanel/scen/imagegraphicsview.cpp b/libimageviewer/viewpanel/scen/imagegraphicsview.cpp
index 71eb0702..c70882cc 100644
--- a/libimageviewer/viewpanel/scen/imagegraphicsview.cpp
+++ b/libimageviewer/viewpanel/scen/imagegraphicsview.cpp
@@ -14,6 +14,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -36,6 +37,7 @@
#include "../contents/morepicfloatwidget.h"
#include "imageengine.h"
#include "service/mtpfileproxy.h"
+#include "service/aimodelservice.h"
#include
#include
@@ -248,7 +250,7 @@ void LibImageGraphicsView::clear()
void LibImageGraphicsView::setImage(const QString &path, const QImage &image)
{
// m_spinner 生命周期由 scene() 管理
- m_spinner = nullptr;
+ hideSpinner();
//默认多页图的按钮显示为false
if (m_morePicFloatWidget) {
@@ -264,11 +266,23 @@ void LibImageGraphicsView::setImage(const QString &path, const QImage &image)
MtpFileProxy::FileState state = MtpFileProxy::instance()->state(path);
bool needProxyLoad = MtpFileProxy::instance()->contains(path) && (MtpFileProxy::Loading == state);
+ bool delayLoad = needProxyLoad;
+ // 判断是否为AI模型处理图片,需要延迟加载
+ auto enhanceState = AIModelService::instance()->enhanceState(path);
+ bool imageEnhance = (AIModelService::None != enhanceState);
+ delayLoad |= (AIModelService::Loading == enhanceState);
+
//检测数据缓存,如果存在,则使用缓存
imageViewerSpace::ItemInfo info;
- if (!needProxyLoad) {
+ if (needProxyLoad) {
+ // 不获取数据
+ } else if (imageEnhance) {
+ QString sourceFile = AIModelService::instance()->sourceFilePath(path);
+ info = LibCommonService::instance()->getImgInfoByPath(sourceFile);
+ } else {
info = LibCommonService::instance()->getImgInfoByPath(path);
}
+
m_bRoate = ImageEngine::instance()->isRotatable(path); //是否可旋转
m_loadPath = path;
@@ -294,8 +308,8 @@ void LibImageGraphicsView::setImage(const QString &path, const QImage &image)
//the judge way to solve the problem
imageViewerSpace::ImageType Type = info.imageType;
- if (needProxyLoad) {
- // MTP代理文件默认为空,后续加载处理
+ if (delayLoad) {
+ // 延迟加载,MTP代理文件默认为空,后续加载处理
Type = imageViewerSpace::ImageTypeBlank;
} else if (Type == imageViewerSpace::ImageTypeBlank) {
Type = LibUnionImage_NameSpace::getImageType(path);
@@ -344,6 +358,11 @@ void LibImageGraphicsView::setImage(const QString &path, const QImage &image)
}, Qt::QueuedConnection);
m_newImageLoadPhase = FullFinish;
} else {
+ QPixmap previousPix;
+ if (imageEnhance && m_pixmapItem) {
+ previousPix = m_pixmapItem->pixmap();
+ }
+
//当传入的image无效时,需要重新读取数据
m_pixmapItem = nullptr;
m_movieItem = nullptr;
@@ -354,96 +373,37 @@ void LibImageGraphicsView::setImage(const QString &path, const QImage &image)
if (image.isNull()) {
QPixmap pix ;
if (!info.image.isNull()) {
- QImageReader imagreader(path); //取原图的分辨率
- int w = imagreader.size().width();
- int h = imagreader.size().height();
-
- int wScale = 0;
- int hScale = 0;
- int wWindow = 0;
- int hWindow = 0;
- if (QApplication::activeWindow()) {
- wWindow = QApplication::activeWindow()->width() * devicePixelRatioF() ;
- hWindow = (QApplication::activeWindow()->height() - TITLEBAR_HEIGHT * 2) * devicePixelRatioF() ;
- } else {
- wWindow = 1300;
- hWindow = 848;
- }
-
- if (w >= wWindow) {
- wScale = wWindow;
- hScale = wScale * h / w;
- if (hScale > hWindow) {
- hScale = hWindow;
- wScale = hScale * w / h;
- }
- } else if (h >= hWindow) {
- hScale = hWindow;
- wScale = hScale * w / h;
- if (wScale >= wWindow) {
- wScale = wWindow;
- hScale = wScale * h / w;
- }
- } else {
- wScale = w;
- hScale = h;
- }
- if (wScale == 0 || wScale == -1) { //进入这个地方说明QImageReader未识别出图片
- if (info.imgOriginalWidth > wWindow || info.imgOriginalHeight > hWindow) {
- wScale = wWindow;
- hScale = hWindow;
- } else {
- wScale = info.imgOriginalWidth;
- hScale = info.imgOriginalHeight;
- }
- }
-
- pix = QPixmap::fromImage(info.image).scaled(wScale, hScale, Qt::KeepAspectRatio);
-
- //存在缩放比问题需要setDevicePixelRatio
-// if (wScale < wWindow && hScale < hWindow) {
- pix.setDevicePixelRatio(devicePixelRatioF());
-// }
+ // 获取用于模糊的图片
+ pix = getBlurPixmap(path, info, previousPix);
}
- if (pix.isNull()) {
- //spinner
- if (!m_spinner) {
- m_spinner = new DSpinner;
- m_spinner->setFixedSize(SPINNER_SIZE);
- }
- m_spinner->start();
-
- QWidget *w = new QWidget();
- w->setFixedSize(SPINNER_SIZE);
- QHBoxLayout *hLayout = new QHBoxLayout;
- hLayout->setMargin(0);
- hLayout->setSpacing(0);
- hLayout->addWidget(m_spinner, 0, Qt::AlignCenter);
- w->setLayout(hLayout);
- // Make sure item show in center of view after reload
- setSceneRect(w->rect());
- s->addWidget(w);
- }
m_pixmapItem = new LibGraphicsPixmapItem(pix);
m_pixmapItem->setTransformationMode(Qt::SmoothTransformation);
- // Make sure item show in center of view after reload
- if (!m_blurEffect) {
- m_blurEffect = new QGraphicsBlurEffect(this);
+ if (delayLoad && imageEnhance) {
+ // 图像增强使用 60% 透明度蒙版效果,不同主题,白色/黑色
+ LibGraphicsMaskItem *maskItem = new LibGraphicsMaskItem(m_pixmapItem);
+ maskItem->setRect(m_pixmapItem->boundingRect());
+ } else {
+ // 设置加载图片模糊效果
+ // Make sure item show in center of view after reload
+ if (!m_blurEffect) {
+ m_blurEffect = new QGraphicsBlurEffect(this);
+ m_blurEffect->setBlurRadius(5);
+ m_blurEffect->setBlurHints(QGraphicsBlurEffect::PerformanceHint);
+ }
+ m_pixmapItem->setGraphicsEffect(m_blurEffect);
}
- m_blurEffect->setBlurRadius(5);
- m_blurEffect->setBlurHints(QGraphicsBlurEffect::PerformanceHint);
- m_pixmapItem->setGraphicsEffect(m_blurEffect);
//如果缩略图不为空,则区域变为m_pixmapItem
if (!pix.isNull()) {
setSceneRect(m_pixmapItem->boundingRect());
}
- // 使用 MTP 代理文件时,需等待代理文件创建完成 createProxyFileFinished() ,完成后调用 onLoadTimerTimeout()
+ // 使用 MTP 代理文件,需等待代理文件创建完成 createProxyFileFinished() ,
+ // 或其他AI模型处理等延迟处理,完成后调用 onLoadTimerTimeout()
//第一次打开直接启动,不使用延时300ms
- if (!needProxyLoad) {
+ if (!delayLoad) {
if (m_isFistOpen) {
onLoadTimerTimeout();
m_isFistOpen = false;
@@ -453,7 +413,16 @@ void LibImageGraphicsView::setImage(const QString &path, const QImage &image)
}
scene()->addItem(m_pixmapItem);
- emit imageChanged(path);
+
+ // 没有可用的图片,设置选转加载图标
+ // AI图像增强时,允许同时存在
+ if (pix.isNull() || delayLoad) {
+ addLoadSpinner(imageEnhance);
+ }
+
+ if (!imageEnhance) {
+ emit imageChanged(path);
+ }
QMetaObject::invokeMethod(this, [ = ]() {
resetTransform();
}, Qt::QueuedConnection);
@@ -473,6 +442,7 @@ void LibImageGraphicsView::setImage(const QString &path, const QImage &image)
emit hideNavigation();
m_newImageLoadPhase = FullFinish;
}
+
if (Type == imageViewerSpace::ImageTypeMulti) {
if (!m_morePicFloatWidget) {
initMorePicWidget();
@@ -1194,10 +1164,8 @@ bool LibImageGraphicsView::event(QEvent *event)
void LibImageGraphicsView::onCacheFinish()
{
- if (m_spinner) {
- m_spinner->stop();
- m_spinner->hide();
- }
+ hideSpinner();
+
QVariantList vl = m_watcher.result();
if (vl.length() == 2) {
const QString path = vl.first().toString();
@@ -1224,8 +1192,12 @@ void LibImageGraphicsView::onCacheFinish()
emit imageChanged(path);
this->update();
m_newImageLoadPhase = FullFinish;
+
+ // AI修图 图像增强屏蔽更新缩略图和图像信息,以准确取得原始图片信息
+ bool currentImageEnhance = AIModelService::instance()->isTemporaryFile(path);
+
//刷新缩略图
- if (!pixmap.isNull()) {
+ if (!pixmap.isNull() && !currentImageEnhance) {
QPixmap thumbnailPixmap;
if (0 != pixmap.height() && 0 != pixmap.width() && (pixmap.height() / pixmap.width()) < 10 && (pixmap.width() / pixmap.height()) < 10) {
bool cache_exist = false;
@@ -1464,8 +1436,132 @@ void LibImageGraphicsView::OnFinishPinchAnimal()
titleBarControl();
}
+/**
+ @brief 取得用于加载 \a path 文件过程中的模糊图片,此图片通过缓存 \a info 中的缩略图放大取得。
+ 图片还未进行模糊,而是通过设置 QGraphicsBlurEffect 实现
+ */
+QPixmap LibImageGraphicsView::getBlurPixmap(const QString &path, const imageViewerSpace::ItemInfo &info, const QPixmap &previousPix)
+{
+ QPixmap pix;
+ QImageReader imagreader(path); //取原图的分辨率
+ int w = imagreader.size().width();
+ int h = imagreader.size().height();
+
+ int wScale = 0;
+ int hScale = 0;
+ int wWindow = 0;
+ int hWindow = 0;
+ if (QApplication::activeWindow()) {
+ wWindow = static_cast(QApplication::activeWindow()->width() * devicePixelRatioF());
+ hWindow = static_cast((QApplication::activeWindow()->height() - TITLEBAR_HEIGHT * 2) * devicePixelRatioF());
+ } else {
+ wWindow = static_cast(this->width() * devicePixelRatioF());
+ hWindow = static_cast((this->height() - TITLEBAR_HEIGHT * 2) * devicePixelRatioF());
+ }
+
+ if (w >= wWindow) {
+ wScale = wWindow;
+ hScale = wScale * h / w;
+ if (hScale > hWindow) {
+ hScale = hWindow;
+ wScale = hScale * w / h;
+ }
+ } else if (h >= hWindow) {
+ hScale = hWindow;
+ wScale = hScale * w / h;
+ if (wScale >= wWindow) {
+ wScale = wWindow;
+ hScale = wScale * h / w;
+ }
+ } else {
+ wScale = w;
+ hScale = h;
+ }
+ if (wScale == 0 || wScale == -1) { //进入这个地方说明QImageReader未识别出图片
+ if (info.imgOriginalWidth > wWindow || info.imgOriginalHeight > hWindow) {
+ wScale = wWindow;
+ hScale = hWindow;
+ } else {
+ wScale = info.imgOriginalWidth;
+ hScale = info.imgOriginalHeight;
+ }
+ }
+
+ if (previousPix.isNull()) {
+ pix = QPixmap::fromImage(info.image).scaled(wScale, hScale, Qt::KeepAspectRatio);
+ } else {
+ pix = previousPix.scaled(wScale, hScale, Qt::KeepAspectRatio);
+ }
+
+ // 存在缩放比问题需要setDevicePixelRatio
+ pix.setDevicePixelRatio(devicePixelRatioF());
+ return pix;
+}
+
+/**
+ @brief 设置图片旋转加载图标,当图片无缩略图,无法使用模糊加载效果时,使用此加载器显示加载效果。
+ \a enhanceImage 用于 AI 图像增强时使用,显示不同文案
+ @note 加载控件由QVBoxLayout布局管理,而不是scene管理,当重新进入 setImage() 时,会自动隐藏
+ */
+void LibImageGraphicsView::addLoadSpinner(bool enhanceImage)
+{
+ if (!m_spinner) {
+ m_spinner = new DSpinner(this);
+ m_spinner->setFixedSize(SPINNER_SIZE);
+
+ QWidget *w = new QWidget(this);
+ w->setFixedSize(SPINNER_SIZE);
+ QVBoxLayout *hLayout = new QVBoxLayout;
+ hLayout->setMargin(0);
+ hLayout->setSpacing(0);
+ hLayout->addWidget(m_spinner, 0, Qt::AlignCenter);
+
+ // 图像增强增加文案,默认隐藏
+ w->setFixedWidth(300);
+ w->setFixedHeight(70);
+ m_spinnerLabel = new QLabel(w);
+ m_spinnerLabel->setText(tr("AI retouching in progress, please wait..."));
+ m_spinnerLabel->setVisible(false);
+ hLayout->addWidget(m_spinnerLabel, 1, Qt::AlignBottom | Qt::AlignHCenter);
+
+ w->setLayout(hLayout);
+
+ if (!this->layout()) {
+ QVBoxLayout *lay = new QVBoxLayout;
+ lay->setAlignment(Qt::AlignCenter);
+ this->setLayout(lay);
+ }
+ this->layout()->addWidget(w);
+ }
+
+ m_spinnerLabel->setVisible(enhanceImage);
+
+ m_spinner->setVisible(true);
+ m_spinner->start();
+}
+
+/**
+ @brief 隐藏加载图标和提示
+ */
+void LibImageGraphicsView::hideSpinner()
+{
+ if (m_spinner) {
+ m_spinner->stop();
+ m_spinner->hide();
+ }
+
+ if (m_spinnerLabel) {
+ m_spinnerLabel->hide();
+ }
+}
+
void LibImageGraphicsView::wheelEvent(QWheelEvent *event)
{
+ // 加载过程不可缩放
+ if (m_spinner && m_spinner->isVisible()) {
+ return;
+ }
+
if ((event->modifiers() == Qt::ControlModifier)) {
if (event->delta() > 0) {
emit previousRequested();
diff --git a/libimageviewer/viewpanel/scen/imagegraphicsview.h b/libimageviewer/viewpanel/scen/imagegraphicsview.h
index 55848c51..98e3c850 100644
--- a/libimageviewer/viewpanel/scen/imagegraphicsview.h
+++ b/libimageviewer/viewpanel/scen/imagegraphicsview.h
@@ -34,6 +34,7 @@ class QPinchGesture;
class QSwipeGesture;
class LibImageSvgItem;
class MorePicFloatWidget;
+class QLabel;
QT_END_NAMESPACE
@@ -190,6 +191,12 @@ private slots:
* 旋转图片松开手指回到特殊位置结束动画槽函数
*/
void OnFinishPinchAnimal();
+
+private:
+ QPixmap getBlurPixmap(const QString &path, const imageViewerSpace::ItemInfo &info, const QPixmap &previousPix);
+ void addLoadSpinner(bool enhanceImage = false);
+ void hideSpinner();
+
private:
bool m_isFitImage = false;
bool m_isFitWindow = false;
@@ -251,6 +258,7 @@ private slots:
//加载旋转
DSpinner *m_spinner{nullptr};
+ QLabel *m_spinnerLabel = nullptr;
int TITLEBAR_HEIGHT = 50;
//单击时间
diff --git a/libimageviewer/viewpanel/viewpanel.cpp b/libimageviewer/viewpanel/viewpanel.cpp
index 1fc401e7..0e7692b9 100644
--- a/libimageviewer/viewpanel/viewpanel.cpp
+++ b/libimageviewer/viewpanel/viewpanel.cpp
@@ -39,11 +39,12 @@
#include "service/imagedataservice.h"
#include "service/mtpfileproxy.h"
#include "unionimage/imageutils.h"
+#include "service/aimodelservice.h"
+#include "contents/aienhancefloatwidget.h"
-const QString IMAGE_TMPPATH = QDir::homePath() +
- "/.config/deepin/deepin-image-viewer/";
+const QString IMAGE_TMPPATH = QDir::homePath() + "/.config/deepin/deepin-image-viewer/";
-const int BOTTOM_TOOLBAR_HEIGHT = 80; //底部工具看高
+const int BOTTOM_TOOLBAR_HEIGHT = 80; //底部工具看高
const int BOTTOM_SPACING = 5; //底部工具栏与底部边缘距离
const int RT_SPACING = 10;
const int TOP_TOOLBAR_HEIGHT = 50;
@@ -116,6 +117,11 @@ LibViewPanel::LibViewPanel(AbstractTopToolbar *customToolbar, QWidget *parent)
initThumbnailWidget();
initConnect();
+ if (AIModelService::instance()->isValid()) {
+ // 创建按钮
+ createAIBtn();
+ }
+
setAcceptDrops(true);
// initExtensionPanel();
@@ -267,6 +273,13 @@ void LibViewPanel::initConnect()
m_dirWatcher = new QFileSystemWatcher(this);
connect(m_dirWatcher, &QFileSystemWatcher::directoryChanged, this, &LibViewPanel::slotsDirectoryChanged);
+ // AI图像增强
+ if (AIModelService::instance()->isValid()) {
+ connect(AIModelService::instance(), &AIModelService::enhanceStart, this, &LibViewPanel::onEnhanceStart);
+ connect(AIModelService::instance(), &AIModelService::enhanceReload, this, &LibViewPanel::onEnhanceReload);
+ connect(AIModelService::instance(), &AIModelService::enhanceEnd, this, &LibViewPanel::onEnhanceEnd);
+ }
+
// DTK 在 5.6.4 后提供紧凑模式接口,调整控件大小
#ifdef DTKWIDGET_CLASS_DSizeMode
connect(DGuiApplicationHelper::instance(),
@@ -446,6 +459,11 @@ void LibViewPanel::updateMenuContent(const QString &path)
if (currentPath.isEmpty()) {
currentPath = m_currentPath;
}
+
+ if (AIModelService::instance()->isTemporaryFile(m_currentPath)) {
+ currentPath = m_currentPath;
+ }
+
QFileInfo info(currentPath);
bool isReadable = info.isReadable() ; //是否可读
@@ -555,6 +573,10 @@ void LibViewPanel::updateMenuContent(const QString &path)
m_menu->addMenu(am);
}
m_menu->addSeparator();
+
+ // 添加AI模型选项,仅处理静态图
+ addAIMenu();
+
if (isAlbum && isReadable) {
appendAction(IdExport, tr("Export"), ss("Export", "Ctrl+E")); //导出
}
@@ -1414,6 +1436,13 @@ void LibViewPanel::slotChangeShowTopBottom()
slotBottomMove();
}
+bool LibViewPanel::event(QEvent *e)
+{
+
+
+ return QFrame::event(e);
+}
+
bool LibViewPanel::slotOcrPicture()
{
if (!m_ocrInterface) {
@@ -1858,6 +1887,12 @@ void LibViewPanel::onMenuItemClicked(QAction *action)
emit ImageEngine::instance()->sigUpdateCollectBtn();
break;
}
+ case IdImageEnhance: {
+ // 调用进行图片增强
+ int enhanceModel = action->property("EnhanceModel").toInt();
+ triggerImageEnhance(currentpath, enhanceModel);
+ break;
+ }
default:
break;
}
@@ -1932,12 +1967,36 @@ void LibViewPanel::openImg(int index, QString path)
m_view->slotRotatePixCurrent();
m_view->setImage(path);
m_view->resetTransform();
- QFileInfo info(path);
+
+ bool currentEnhance = AIModelService::instance()->isTemporaryFile(path);
+ setAIBtnVisible(currentEnhance);
+
+ QFileInfo info(AIModelService::instance()->sourceFilePath(path));
m_topToolbar->setMiddleContent(info.fileName());
+
+ if (AIModelService::instance()->isValid()) {
+ // 判断当前图片是否为图像增强图片
+ bool previousEnhanced = AIModelService::instance()->isTemporaryFile(m_currentPath);
+ if (previousEnhanced) {
+ if (AIModelService::instance()->isWaitSave()) {
+ return;
+ }
+
+ // 提示是否保存
+ if (!notNeedNotifyEnhanceSave) {
+ AIModelService::instance()->saveFileDialog(m_currentPath);
+ }
+ }
+ // 打开其他图片时,清理之前的状态
+ Q_EMIT AIModelService::instance()->clearPreviousEnhance();
+ }
+
m_currentPath = path;
+ if (!currentEnhance) {
+ loadThumbnails(path);
+ }
+
//刷新收藏按钮
-// qDebug() << index;
- loadThumbnails(path);
emit ImageEngine::instance()->sigUpdateCollectBtn();
updateMenuContent(path);
@@ -2021,8 +2080,6 @@ void LibViewPanel::resizeEvent(QResizeEvent *e)
//不需要动画滑动
noAnimationBottomMove();
-
-
}
void LibViewPanel::showEvent(QShowEvent *e)
@@ -2140,3 +2197,203 @@ bool LibViewPanel::eventFilter(QObject *o, QEvent *e)
return QFrame::eventFilter(o, e);
}
+
+/**
+ @brief 添加AI模型增强选项
+ */
+void LibViewPanel::addAIMenu()
+{
+ if (m_menu && AIModelService::instance()->isValid()) {
+ // 缓存的支持模型列表<名称,模型>
+ QList> modelList = AIModelService::instance()->supportModel();
+ if (!modelList.isEmpty()) {
+ // Image enhance
+ QMenu *enhanceMenu = m_menu->addMenu(tr("AI retouching"));
+
+ // 模型可能动态变更
+ for (const QPair &model : modelList) {
+ // 命名空间作用,需要指定 QObject::tr() 调用翻译
+ QAction *ac = enhanceMenu->addAction(QObject::tr(model.second.toUtf8().data()));
+ ac->setProperty("MenuID", IdImageEnhance);
+ ac->setProperty("EnhanceModel", model.first);
+ }
+
+ m_menu->addSeparator();
+ }
+ }
+}
+
+/**
+ @brief 创建右侧的AI按钮浮动栏
+ */
+void LibViewPanel::createAIBtn()
+{
+ if (!m_AIFloatBar) {
+ m_AIFloatBar = new AIEnhanceFloatWidget(this);
+
+ connect(m_AIFloatBar, &AIEnhanceFloatWidget::reset, this, &LibViewPanel::resetAIEnhanceImage);
+ connect(m_AIFloatBar, &AIEnhanceFloatWidget::save, this, [this](){
+ AIModelService::instance()->saveEnhanceFile(m_currentPath);
+ resetAIEnhanceImage();
+ });
+ connect(m_AIFloatBar, &AIEnhanceFloatWidget::saveAs, this, [this](){
+ AIModelService::instance()->saveEnhanceFileAs(m_currentPath);
+ resetAIEnhanceImage();
+ });
+ }
+}
+
+/**
+ @brief 设置AI按钮浮动栏是否显示
+ */
+void LibViewPanel::setAIBtnVisible(bool visible)
+{
+ if (m_AIFloatBar) {
+ m_AIFloatBar->setVisible(visible);
+ }
+}
+
+/**
+ @brief 触发 \a filePath 图像增强,根据不同选项调用不同模型 \a modelID
+ */
+void LibViewPanel::triggerImageEnhance(const QString &filePath, int modelID)
+{
+ // 判断原文件(可能删除)是否可用
+ QString source = AIModelService::instance()->sourceFilePath(filePath);
+ auto error = AIModelService::instance()->modelEnabled(modelID, source);
+ if (AIModelService::instance()->detectErrorAndNotify(this->parentWidget(), error, filePath)) {
+ return;
+ }
+
+ QString output = AIModelService::instance()->imageProcessing(filePath, modelID, m_view->image());
+ if (output.isEmpty()) {
+ return;
+ }
+ m_view->setImage(output, QImage());
+}
+
+/**
+ @brief 执行图像增强时,根据 \a block 屏蔽界面按钮和快捷键控制
+ */
+void LibViewPanel::blockInputControl(bool block)
+{
+ // 屏蔽工具栏和右键菜单
+ m_bottomToolbar->setEnabled(!block);
+ m_thumbnailWidget->setEnabled(!block);
+
+ if (block) {
+ setContextMenuPolicy(Qt::NoContextMenu);
+ if (m_menu) {
+ m_menu->clear();
+ qDeleteAll(this->actions());
+ }
+ } else {
+ // 右键菜单设置图片将自动刷新
+ setContextMenuPolicy(Qt::CustomContextMenu);
+ }
+
+ // 部分快捷键绑定到 viewpanel , Ctrl+O 绑定在主窗口
+ auto shortcutList = this->findChildren("");
+ for (auto shortcut : shortcutList) {
+ shortcut->setEnabled(!block);
+ }
+
+ auto win = window();
+ if (win) {
+ shortcutList = win->findChildren("");
+ for (auto shortcut : shortcutList) {
+ shortcut->setEnabled(!block);
+ }
+ }
+}
+
+/**
+ @brief 复位当前AI修图增强的图像
+ */
+void LibViewPanel::resetAIEnhanceImage()
+{
+ if (m_AIFloatBar) {
+ m_AIFloatBar->setVisible(false);
+ }
+
+ // 还原原始图片
+ QString source = AIModelService::instance()->sourceFilePath(m_currentPath);
+
+ notNeedNotifyEnhanceSave = true;
+ openImg(0, source);
+ notNeedNotifyEnhanceSave = false;
+}
+
+/**
+ @brief AI修图图像增强开始,屏蔽界面设置
+ */
+void LibViewPanel::onEnhanceStart()
+{
+ m_AIEnhancing = true;
+
+ blockInputControl(true);
+ setAIBtnVisible(false);
+}
+
+/**
+ @brief 接收到 \a output 文件的AI修图重试信号,再次屏蔽界面设置
+ */
+void LibViewPanel::onEnhanceReload(const QString &output)
+{
+ // 仅会处理当前图片,增强失败时会还原为原始文件路径
+ if (m_currentPath != AIModelService::instance()->sourceFilePath(output)) {
+ return;
+ };
+
+ // 设置临时图片
+ m_view->setImage(output, QImage());
+
+ m_AIEnhancing = true;
+
+ blockInputControl(true);
+ setAIBtnVisible(false);
+}
+
+/**
+ @brief AI修图调用结束,根据输出文件 \a output 的增强状态 \a state 判断是否界面替换 \a source 文件展示。
+ 若图像增强失败,则会还原为原始的图像文件 \a source 。
+ */
+void LibViewPanel::onEnhanceEnd(const QString &source, const QString &output, int state)
+{
+ // 仅会处理当前图片
+ if (source != AIModelService::instance()->sourceFilePath(m_currentPath)) {
+ if (m_AIEnhancing) {
+ qWarning() << qPrintable("Detect error! receive previous procssing file but still in enhancing state.");
+ blockInputControl(false);
+ }
+ return;
+ };
+
+ QString procPath;
+ switch (state) {
+ case AIModelService::LoadSucc: {
+ procPath = output;
+ break;
+ }
+ case AIModelService::LoadFailed: {
+ procPath = source;
+ AIModelService::instance()->detectErrorAndNotify(this->parentWidget(), AIModelService::LoadFiledError, output);
+ break;
+ }
+ case AIModelService::NotDetectFace: {
+ procPath = source;
+ AIModelService::instance()->detectErrorAndNotify(this->parentWidget(), AIModelService::NotDetectFaceError, output);
+ break;
+ }
+ default:
+ return;
+ }
+
+ // Note: 仅变更了运行时的文件名,而 m_bottomToolbar->getCurrentItemInfo() 的路径信息并未更新
+ notNeedNotifyEnhanceSave = true;
+ openImg(0, procPath);
+ notNeedNotifyEnhanceSave = false;
+
+ blockInputControl(false);
+ m_AIEnhancing = false;
+}
diff --git a/libimageviewer/viewpanel/viewpanel.h b/libimageviewer/viewpanel/viewpanel.h
index 75c33eb0..97c73f88 100644
--- a/libimageviewer/viewpanel/viewpanel.h
+++ b/libimageviewer/viewpanel/viewpanel.h
@@ -30,6 +30,7 @@ class LibSlideShowPanel;
class QPropertyAnimation;
class LockWidget;
class ThumbnailWidget;
+class AIEnhanceFloatWidget;
class LibViewPanel : public QFrame
{
@@ -176,6 +177,8 @@ public slots:
void slotChangeShowTopBottom();
protected:
+ bool event(QEvent *e) override;
+
void resizeEvent(QResizeEvent *e) override;
void showEvent(QShowEvent *e) override;
void paintEvent(QPaintEvent *event) override;
@@ -200,6 +203,19 @@ public slots:
//刷新缩略图
void updateThumbnail(QPixmap pix, const QSize &originalSize);
+
+private:
+ // AI图像增强相关接口
+ void addAIMenu();
+ void createAIBtn();
+ void setAIBtnVisible(bool visible);
+ void triggerImageEnhance(const QString &filePath, int modelID);
+ void blockInputControl(bool block);
+ Q_SLOT void resetAIEnhanceImage();
+ Q_SLOT void onEnhanceStart();
+ Q_SLOT void onEnhanceReload(const QString &output);
+ Q_SLOT void onEnhanceEnd(const QString &source, const QString &output, int state);
+
private:
DStackedWidget *m_stack = nullptr;
LibImageGraphicsView *m_view = nullptr;
@@ -253,5 +269,10 @@ public slots:
QSize m_windowSize;
int m_windowX = 0;
int m_windowY = 0;
+
+ // AI 功能
+ bool m_AIEnhancing = false;
+ bool notNeedNotifyEnhanceSave = false; // 用于控制是否需要提示保存增强图片
+ AIEnhanceFloatWidget *m_AIFloatBar = nullptr; // 按钮浮动窗口
};
#endif // VIEWPANEL_H
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index a171f46f..bb0c3513 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.0.0)
project(image-editor-test VERSION 0.1)
+ADD_COMPILE_OPTIONS(-fno-access-control)
+
#"option"用来定义宏,"ON"表示打开,"OFF"表示关闭
option (LITE_DIV "Use tutorial provided math implementation" ON)
add_definitions( -DLITE_DIV )
diff --git a/tests/test_aimodelservice.cpp b/tests/test_aimodelservice.cpp
new file mode 100644
index 00000000..2fe5a968
--- /dev/null
+++ b/tests/test_aimodelservice.cpp
@@ -0,0 +1,102 @@
+// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include
+#include
+
+#include "service/aimodelservice.h"
+#include "service/aimodelservice_p.h"
+
+class TestAIModelService : public testing::Test
+{
+protected:
+ void SetUp();
+ void TearDown() {}
+
+ AIModelService *ins = nullptr;
+};
+
+void TestAIModelService::SetUp()
+{
+ ins = AIModelService::instance();
+}
+
+TEST_F(TestAIModelService, TemporaryFile_Contains_Pass)
+{
+ ins->dptr->enhanceCache.clear();
+ EnhancePtr ptr(new EnhanceInfo("source", "output", "model"));
+ ins->dptr->enhanceCache.insert("output", ptr);
+
+ EXPECT_FALSE(ins->isTemporaryFile("source"));
+ EXPECT_TRUE(ins->isTemporaryFile("output"));
+ EXPECT_EQ(QString("source"), ins->sourceFilePath("output"));
+
+ ins->dptr->enhanceCache.clear();
+}
+
+TEST_F(TestAIModelService, ImageProcessing_ErrorFile_Failed)
+{
+ ModelPtr mptr(new ModelInfo);
+ mptr->modelID = 0;
+ mptr->model = "model";
+ ins->dptr->mapModelInfo.insert(mptr->modelID, mptr);
+
+ QString outfile = ins->imageProcessing("source", mptr->modelID, QImage());
+ // 等待处理结束,涉及条件变量等待!
+ ins->dptr->enhanceWatcher.waitForFinished();
+
+ auto enhancePtr = ins->dptr->enhanceWatcher.result();
+ EXPECT_EQ(enhancePtr->source, QString("source"));
+ EXPECT_EQ(enhancePtr->output, outfile);
+
+ EXPECT_FALSE(QFile::exists(outfile));
+ EXPECT_FALSE(ins->dptr->enhanceCache.isEmpty());
+}
+
+TEST_F(TestAIModelService, OnDBusEnhanceEnd_NotExist_Failed)
+{
+ QString source;
+ QString output;
+ AIModelService::State state;
+ auto conn =
+ QObject::connect(ins, &AIModelService::enhanceEnd, [&](const QString &src, const QString &out, AIModelService::State s) {
+ source = src;
+ output = out;
+ state = s;
+ });
+
+ ins->dptr->enhanceCache.clear();
+ ins->onDBusEnhanceEnd("output", 0);
+ EXPECT_TRUE(source.isEmpty());
+
+ EnhancePtr ptr(new EnhanceInfo("source", "output", "model"));
+ ptr->state.store(AIModelService::LoadSucc);
+ ins->dptr->enhanceCache.insert("output", ptr);
+ ins->onDBusEnhanceEnd("output", 0);
+ EXPECT_EQ(AIModelService::LoadFailed, ptr->state.loadAcquire());
+
+ EXPECT_EQ(source, QString("source"));
+ EXPECT_EQ(output, QString("output"));
+ EXPECT_EQ(state, ptr->state.loadAcquire());
+
+ QObject::disconnect(conn);
+}
+
+TEST_F(TestAIModelService, CheckConvertFile_NewImage_Pass)
+{
+ ins->dptr->convertCache.clear();
+ QImage nullImage;
+ QString tmpImage = ins->checkConvertFile("localtest.png", nullImage);
+ EXPECT_TRUE(tmpImage.isEmpty());
+
+ QImage contentImage(20, 20, QImage::Format_ARGB32);
+ contentImage.fill(Qt::red);
+ tmpImage = ins->checkConvertFile("localtest.png", contentImage);
+ EXPECT_FALSE(tmpImage.isEmpty());
+ EXPECT_TRUE(QFile::exists(tmpImage));
+ EXPECT_TRUE(ins->dptr->convertCache.contains("localtest.png"));
+ EXPECT_EQ(tmpImage, ins->dptr->convertCache.value("localtest.png"));
+
+ ins->dptr->convertCache.clear();
+}
diff --git a/tests/test_movieservice.cpp b/tests/test_movieservice.cpp
index c207c75a..87de6f91 100644
--- a/tests/test_movieservice.cpp
+++ b/tests/test_movieservice.cpp
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd.
+// SPDX-FileCopyrightText: 2022 - 2023 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later
@@ -39,7 +39,7 @@ TEST(movieservice, movieCover)
auto image_1 = MovieService::instance()->getMovieCover(QUrl::fromLocalFile(filePath), QApplication::applicationDirPath() + QDir::separator());
//分接口
- auto image_2 = MovieService::instance()->getMovieCover_gstreamer(QUrl::fromLocalFile(filePath));
+ auto image_2 = MovieService::instance()->getMovieCover_ffmpegthumbnailerlib(QUrl::fromLocalFile(filePath));
auto image_3 = MovieService::instance()->getMovieCover_ffmpegthumbnailer(QUrl::fromLocalFile(filePath), QApplication::applicationDirPath() + QDir::separator());
//简单判断