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 @@ + + + + warning 2 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + file save as + + + + + + + + + \ 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 @@ + + + reset + + + + + + + + + \ 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 @@ + + + save + + + + + + + + + \ 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 + + Save + + + + Restore + + + + + AIModelService + + Save + + + ExtensionPanel @@ -57,6 +75,18 @@ Favorite Favorite + + Image enhance + + + + Reset + + + + Save + + LockWidget @@ -236,12 +266,36 @@ day - day + day Photo info Photo info + + cartoon2d + + + + cartoon3d + + + + hand + + + + sketch + + + + coloring + + + + super-resolution + + RenameDialog @@ -285,6 +339,29 @@ Exit + + TextToImageDialog + + Save as + + + + Generate + + + + Text to image + + + + Prompt + + + + Negative prompt + + + 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 + + Save + 保存 + + + Save as + 另存为 + + + Reprovision + 重置 + + + + AIModelService + + Save + 保存 + + + Save as + 另存为 + + + Cancel + 取 消 + + + Image not saved, Do you want to save it? + 图片未保存,是否保存? + + + Image format is not supported, please switch the image. + 图像格式不支持,请切换图片。 + + + The image resolution exceeds the limit, please switch the image. + 图像分辨率超过限制,请切换图片。 + + + Portrait not detected, switch pictures. + 未检测到人像,请切换图片。 + + ExtensionPanel @@ -12,6 +58,13 @@ Ctrl+I + + LibImageGraphicsView + + AI retouching in progress, please wait... + AI修图中,请稍等... + + LibImageInfoWidget @@ -57,6 +110,18 @@ Favorite 收藏 + + Reset + 还原 + + + Save + 保存 + + + AI retouching + AI修图 + LockWidget @@ -236,12 +301,52 @@ day - + Photo info 照片信息 + + Retry + 重 试 + + + Colorize pictures + 图片上色 + + + Upgrade resolution + 提升分辨率 + + + Blurred background + 模糊背景 + + + Delete background + 删除背景 + + + Hand-drawn cartoons + 手绘漫画 + + + 2D Manga + 2D漫画 + + + Sketch + 素描 + + + Processing failure. + 处理失败。 + + + 3D Manga + 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 + + Save + 保存 + + + Restore + 還原 + + + + AIEnhanceFloatWidget + + Save + 保存 + + + Save as + 另存爲 + + + Reprovision + 還原 + + + + AIModelService + + Save + 保存 + + + Save as + 另存爲 + + + Cancel + 取 消 + + + Image not saved, Do you want to save it? + 圖片未保存,是否保存? + + + Image format is not supported, please switch the image. + 圖像格式不支持,請切換圖片。 + + + The image resolution exceeds the limit, please switch the image. + 圖像分辨率超過限制,請切換圖片。 + + + Portrait not detected, switch pictures. + 未檢測到人像,請切換圖片。 + + ExtensionPanel @@ -12,6 +69,13 @@ Ctrl+I + + LibImageGraphicsView + + AI retouching in progress, please wait... + AI修圖中,請稍等... + + LibImageInfoWidget @@ -57,6 +121,18 @@ Favorite 收藏 + + Reset + 還原 + + + Save + 保存 + + + AI retouching + AI修圖 + LockWidget @@ -236,12 +312,52 @@ day - + Photo info 照片信息 + + Retry + 重 試 + + + Colorize pictures + 圖片上色 + + + Upgrade resolution + 提升分辨率 + + + Blurred background + 模糊背景 + + + Delete background + 刪除背景 + + + Hand-drawn cartoons + 手繪漫畫 + + + 2D Manga + 2D漫畫 + + + Sketch + 素描 + + + Processing failure. + 處理失敗。 + + + 3D Manga + 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 + + Save + 保存 + + + Restore + 还原 + + + + AIEnhanceFloatWidget + + Save + 保存 + + + Save as + 另存為 + + + Reprovision + 還原 + + + + AIModelService + + Save + 保存 + + + Save as + 另存為 + + + Cancel + 取 消 + + + Image not saved, Do you want to save it? + 圖片未保存,是否保存? + + + Image format is not supported, please switch the image. + 圖像格式不支持,請切換圖片。 + + + The image resolution exceeds the limit, please switch the image. + 圖像分辨率超過限制,請切換圖片。 + + + Portrait not detected, switch pictures. + 未檢測到人像,請切換圖片。 + + ExtensionPanel @@ -12,6 +69,13 @@ Ctrl+I + + LibImageGraphicsView + + AI retouching in progress, please wait... + AI修圖中,請稍等... + + LibImageInfoWidget @@ -57,6 +121,18 @@ Favorite 收藏 + + Reset + 還原 + + + Save + 保存 + + + AI retouching + AI修圖 + LockWidget @@ -236,12 +312,52 @@ day - + Photo info 照片訊息 + + Retry + 重 試 + + + Colorize pictures + 圖片上色 + + + Upgrade resolution + 提升分辨率 + + + Blurred background + 模糊背景 + + + Delete background + 刪除背景 + + + Hand-drawn cartoons + 手繪漫畫 + + + 2D Manga + 2D漫畫 + + + Sketch + 素描 + + + Processing failure. + 處理失敗。 + + + 3D Manga + 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()); //简单判断