From 7e4758b8bf099dc42e03bf12cd102846a019a7c0 Mon Sep 17 00:00:00 2001 From: xujin Date: Thu, 23 Jul 2026 19:50:48 +0800 Subject: [PATCH] fix: move image decoding logic from bubble item to notify entity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The image decoding functions (copyLineRGB32, copyLineARGB32, decodeImageFromDBusArgument, decodeImageToBase64, imagePathOfNotification) and the appIcon resolution logic have been moved from BubbleItem to NotifyEntity. This refactoring centralizes image processing in the entity layer, reducing code duplication and enabling reuse of the resolved icon logic in the NotificationManager. The new appIconResolved() method replaces the local logic in BubbleItem::appIcon(), and the manager now pre-sets the resolved icon on the entity before further processing. Log: Moving image decoding from bubble UI to notification entity Influence: 1. Verify that notification icons from image-data/icon_data hints are correctly displayed in bubble popups 2. Test notification icons from appIcon fallback when no hints are provided 3. Verify that the icon resolution in NotificationManager does not affect existing notification behavior 4. Test various image formats (RGB32, ARGB32) and edge cases (invalid data, incomplete images) 5. Verify that base64 data URIs from hints are preserved and displayed correctly 6. Test with applications that send desktop-entry hints and verify icon lookup fix: 将图片解码逻辑从气泡项迁移到通知实体 将图片解码函数(copyLineRGB32、copyLineARGB32、 decodeImageFromDBusArgument、decodeImageToBase64、 imagePathOfNotification)以及应用图标解析逻辑从 BubbleItem 移动 到 NotifyEntity。此重构将图片处理集中在实体层,减少代码重复,并使 NotificationManager 能够重用已解析的图标逻辑。新增的 appIconResolved() 方法替代了 BubbleItem::appIcon() 中的本地逻辑,管理器现在在进一步处理前 预先设置已解析的图标到实体。 Log: 将图片解码从气泡 UI 移至通知实体 Influence: 1. 验证来自 image-data/icon_data 提示的通知图标在气泡弹窗中正确显示 2. 测试未提供提示时使用 appIcon 回退的通知图标 3. 验证 NotificationManager 中的图标解析不影响现有通知行为 4. 测试各种图片格式(RGB32、ARGB32)及边界情况(无效数据、不完整图片) 5. 验证来自提示的 base64 数据 URI 被正确保留和显示 6. 测试发送 desktop-entry 提示的应用,验证图标查找功能 PMS: BUG-370701 --- panels/notification/CMakeLists.txt | 1 + panels/notification/bubble/bubbleitem.cpp | 143 +----------------- panels/notification/center/notifymodel.cpp | 2 +- .../center/notifystagingmodel.cpp | 4 +- panels/notification/common/notifyentity.cpp | 134 ++++++++++++++++ panels/notification/common/notifyentity.h | 4 + 6 files changed, 143 insertions(+), 145 deletions(-) diff --git a/panels/notification/CMakeLists.txt b/panels/notification/CMakeLists.txt index dc7bc1b2c..b7c6414eb 100644 --- a/panels/notification/CMakeLists.txt +++ b/panels/notification/CMakeLists.txt @@ -33,6 +33,7 @@ target_include_directories(ds-notification-shared PUBLIC ) target_link_libraries(ds-notification-shared PUBLIC Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Sql Dtk${DTK_VERSION_MAJOR}::Core ICU::uc diff --git a/panels/notification/bubble/bubbleitem.cpp b/panels/notification/bubble/bubbleitem.cpp index 2111fe117..60d48a6d7 100644 --- a/panels/notification/bubble/bubbleitem.cpp +++ b/panels/notification/bubble/bubbleitem.cpp @@ -21,138 +21,6 @@ Q_DECLARE_LOGGING_CATEGORY(notifyLog) namespace notification { -static inline void copyLineRGB32(QRgb *dst, const char *src, int width) -{ - const char *end = src + width * 3; - for (; src != end; ++dst, src += 3) { - *dst = qRgb(src[0], src[1], src[2]); - } -} - -static inline void copyLineARGB32(QRgb *dst, const char *src, int width) -{ - const char *end = src + width * 4; - for (; src != end; ++dst, src += 4) { - *dst = qRgba(src[0], src[1], src[2], src[3]); - } -} - -static QImage decodeImageFromDBusArgument(const QDBusArgument &arg) -{ - int width, height, rowStride, hasAlpha, bitsPerSample, channels; - QByteArray pixels; - char *ptr; - char *end; - - arg.beginStructure(); - arg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample >> channels >> pixels; - arg.endStructure(); - //qDebug(notifyLog) << width << height << rowStride << hasAlpha << bitsPerSample << channels; - -#define SANITY_CHECK(condition) \ -if (!(condition)) { \ - qWarning(notifyLog) << "Sanity check failed on" << #condition; \ - return QImage(); \ -} - - SANITY_CHECK(width > 0); - SANITY_CHECK(width < 2048); - SANITY_CHECK(height > 0); - SANITY_CHECK(height < 2048); - SANITY_CHECK(rowStride > 0); - -#undef SANITY_CHECK - - QImage::Format format = QImage::Format_Invalid; - void (*fcn)(QRgb *, const char *, int) = nullptr; - if (bitsPerSample == 8) { - if (channels == 4) { - format = QImage::Format_ARGB32; - fcn = copyLineARGB32; - } else if (channels == 3) { - format = QImage::Format_RGB32; - fcn = copyLineRGB32; - } - } - if (format == QImage::Format_Invalid) { - qWarning(notifyLog) << "Unsupported image format (hasAlpha:" << hasAlpha << "bitsPerSample:" << bitsPerSample << "channels:" << channels << ")"; - return QImage(); - } - - QImage image(width, height, format); - ptr = pixels.data(); - end = ptr + pixels.length(); - for (int y = 0; y < height; ++y, ptr += rowStride) { - if (ptr + channels * width > end) { - qWarning(notifyLog) << "Image data is incomplete. y:" << y << "height:" << height; - break; - } - fcn((QRgb *)image.scanLine(y), ptr, width); - } - - return image; -} - -static QString decodeImageToBase64(const QImage &image, const char *format = "PNG") -{ - QByteArray ba; - QBuffer buffer(&ba); - buffer.open(QIODevice::WriteOnly); - image.save(&buffer, format); - - return QString("data:image/%1;base64,%2").arg(QString::fromLatin1(format).toLower()).arg(QString::fromLatin1(ba.toBase64())); -} - -[[maybe_unused]] static QIcon decodeIconFromPath(const QString &arg, const QString &fallback) -{ - DGUI_USE_NAMESPACE; - const QUrl url(arg); - const auto iconUrl = url.isLocalFile() ? url.toLocalFile() : url.url(); - QIcon icon = DIconTheme::findQIcon(iconUrl); - if (!icon.isNull()) { - return icon; - } - return DIconTheme::findQIcon(fallback, DIconTheme::findQIcon("application-x-desktop")); -} - -static QString imagePathOfNotification(const QVariantMap &hints, const QString &appIcon, const QString &appName) -{ - Q_UNUSED(appName) - static const QStringList HintsOrder { - "desktop-entry", - "image-data", - "icon_data" - }; - - QImage img; - QString imageData(appIcon); - for (const auto &hint : HintsOrder) { - const auto &source = hints[hint]; - if (source.isNull()) - continue; - if (source.canConvert()) { - img = decodeImageFromDBusArgument(source.value()); - if (!img.isNull()) - break; - } - imageData = source.toString(); - } - if (img.isNull()) { - // check if imageData is a base64 image data. - QRegularExpression dataUriPattern("^data:image/[a-zA-Z0-9+\\-]+;base64,"); - QRegularExpressionMatch match = dataUriPattern.match(imageData); - if (match.hasMatch()) { - return imageData; - } - } else { - return decodeImageToBase64(img); - } - - // ui can fallback to application-x-desktop icon. - return {}; -} - - BubbleItem::BubbleItem(QObject *parent) : QObject(parent) , m_timeTip(tr("just now")) @@ -195,16 +63,7 @@ QString BubbleItem::appName() const QString BubbleItem::appIcon() const { - // image-data / icon_data hints carry the notification-specific image - const QString iconFromHints = imagePathOfNotification(m_entity.hints(), m_entity.appIcon(), m_entity.appName()); - if (!iconFromHints.isEmpty()) - return iconFromHints; - - if (!m_entity.appIcon().isEmpty()) { - return m_entity.appIcon(); - } - - return {}; + return m_entity.appIconResolved(); } QString BubbleItem::summary() const diff --git a/panels/notification/center/notifymodel.cpp b/panels/notification/center/notifymodel.cpp index 0a5be295e..ac3135dd8 100644 --- a/panels/notification/center/notifymodel.cpp +++ b/panels/notification/center/notifymodel.cpp @@ -702,7 +702,7 @@ QVariant NotifyModel::data(const QModelIndex &index, int role) const } else if (role == NotifyRole::NotifyAppName) { return notify->appName(); } else if (role == NotifyRole::NotifyIconName) { - return notify->entity().appIcon(); + return notify->entity().appIconResolved(); } else if (role == NotifyRole::NotifyTitle) { return notify->entity().summary(); } else if (role == NotifyRole::NotifyContent) { diff --git a/panels/notification/center/notifystagingmodel.cpp b/panels/notification/center/notifystagingmodel.cpp index 458b52a20..303bfc75c 100644 --- a/panels/notification/center/notifystagingmodel.cpp +++ b/panels/notification/center/notifystagingmodel.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024-2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -197,7 +197,7 @@ QVariant NotifyStagingModel::data(const QModelIndex &index, int role) const } else if (role == NotifyRole::NotifyAppName) { return notify->appName(); } else if (role == NotifyRole::NotifyIconName) { - return notify->entity().appIcon(); + return notify->entity().appIconResolved(); } else if (role == NotifyRole::NotifyTitle) { return notify->entity().summary(); } else if (role == NotifyRole::NotifyContent) { diff --git a/panels/notification/common/notifyentity.cpp b/panels/notification/common/notifyentity.cpp index a97e5136f..31613019a 100644 --- a/panels/notification/common/notifyentity.cpp +++ b/panels/notification/common/notifyentity.cpp @@ -8,6 +8,10 @@ #include #include #include +#include +#include +#include +#include #include #include @@ -292,6 +296,136 @@ QString NotifyEntity::bodyIcon() const return QString(); } +static inline void copyLineRGB32(QRgb *dst, const char *src, int width) +{ + const char *end = src + width * 3; + for (; src != end; ++dst, src += 3) { + *dst = qRgb(static_cast(src[0]), static_cast(src[1]), static_cast(src[2])); + } +} + +static inline void copyLineARGB32(QRgb *dst, const char *src, int width) +{ + const char *end = src + width * 4; + for (; src != end; ++dst, src += 4) { + *dst = qRgba(static_cast(src[0]), static_cast(src[1]), static_cast(src[2]), static_cast(src[3])); + } +} + +static QImage decodeImageFromDBusArgument(const QDBusArgument &arg) +{ + int width, height, rowStride, hasAlpha, bitsPerSample, channels; + QByteArray pixels; + char *ptr; + char *end; + + arg.beginStructure(); + arg >> width >> height >> rowStride >> hasAlpha >> bitsPerSample >> channels >> pixels; + arg.endStructure(); + +#define SANITY_CHECK(condition) \ +if (!(condition)) { \ + qWarning(notifyLog) << "Sanity check failed on" << #condition; \ + return QImage(); \ +} + + SANITY_CHECK(width > 0); + SANITY_CHECK(width < 2048); + SANITY_CHECK(height > 0); + SANITY_CHECK(height < 2048); + SANITY_CHECK(rowStride > 0); + +#undef SANITY_CHECK + + QImage::Format format = QImage::Format_Invalid; + void (*fcn)(QRgb *, const char *, int) = nullptr; + if (bitsPerSample == 8) { + if (channels == 4) { + format = QImage::Format_ARGB32; + fcn = copyLineARGB32; + } else if (channels == 3) { + format = QImage::Format_RGB32; + fcn = copyLineRGB32; + } + } + if (format == QImage::Format_Invalid) { + qWarning(notifyLog) << "Unsupported image format (hasAlpha:" << hasAlpha << "bitsPerSample:" << bitsPerSample << "channels:" << channels << ")"; + return QImage(); + } + + QImage image(width, height, format); + ptr = pixels.data(); + end = ptr + pixels.length(); + for (int y = 0; y < height; ++y, ptr += rowStride) { + if (ptr + channels * width > end) { + qWarning(notifyLog) << "Image data is incomplete. y:" << y << "height:" << height; + break; + } + fcn(reinterpret_cast(image.scanLine(y)), ptr, width); + } + + return image; +} + +static QString decodeImageToBase64(const QImage &image, const char *format = "PNG") +{ + QByteArray ba; + QBuffer buffer(&ba); + buffer.open(QIODevice::WriteOnly); + image.save(&buffer, format); + + return QString("data:image/%1;base64,%2").arg(QString::fromLatin1(format).toLower()).arg(QString::fromLatin1(ba.toBase64())); +} + +static QString imagePathOfNotification(const QVariantMap &hints, const QString &appIcon) +{ + static const QStringList HintsOrder { + "desktop-entry", + "image-data", + "icon_data" + }; + + QImage img; + QString imageData(appIcon); + for (const auto &hint : HintsOrder) { + const auto &source = hints[hint]; + if (source.isNull()) + continue; + if (source.canConvert()) { + img = decodeImageFromDBusArgument(source.value()); + if (!img.isNull()) + break; + } + imageData = source.toString(); + } + if (img.isNull()) { + // check if imageData is a base64 image data. + QRegularExpression dataUriPattern("^data:image/[a-zA-Z0-9+\\-]+;base64,"); + QRegularExpressionMatch match = dataUriPattern.match(imageData); + if (match.hasMatch()) { + return imageData; + } + } else { + return decodeImageToBase64(img); + } + + // ui can fallback to application-x-desktop icon. + return {}; +} + +QString NotifyEntity::appIconResolved() const +{ + const QString iconFromHints = imagePathOfNotification(d->hints, d->appIcon); + if (!iconFromHints.isEmpty()) + return iconFromHints; + + if (!d->appIcon.isEmpty()) { + return d->appIcon; + } + + return {}; +} + QString NotifyEntity::convertHintsToString(const QVariantMap &map) { QString text; diff --git a/panels/notification/common/notifyentity.h b/panels/notification/common/notifyentity.h index 9b72ec97b..967280ddb 100644 --- a/panels/notification/common/notifyentity.h +++ b/panels/notification/common/notifyentity.h @@ -93,6 +93,10 @@ class NotifyEntity QString bodyIcon() const; + // Resolves the app icon considering image-data/icon_data hints from the notification spec. + // Returns a base64 data URI if image-data/icon_data hint is present, otherwise returns appIcon. + QString appIconResolved() const; + // Formats a creation time (ms since epoch) as a locale-aware relative // time string. Returns empty string if less than 1 minute or invalid. static QString formatRelativeTime(qint64 ctimeMs);