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);