Compare raw icons by their checksums

Currently, we just skipped the notification comparison, if the
notification had a raw icon attached. This is a bit counterintuitive.

Calculating a checksum of the raw icon's data is the solution.

For that we cache the pixel buffer and introduce a field, which saves
the current icon's id. The icon_id may be a path or a hash.
So you can compare two notifications by their icon_id field regardless
of their icon type by their icon_id field.
This commit is contained in:
Benedikt Heine 2019-01-03 20:17:18 +01:00
parent 8a46b88da9
commit 6f8b53c4e8
8 changed files with 222 additions and 125 deletions

View File

@ -86,8 +86,6 @@ struct dbus_method {
GVariant *parameters, \ GVariant *parameters, \
GDBusMethodInvocation *invocation) GDBusMethodInvocation *invocation)
static struct raw_image *get_raw_image_from_data_hint(GVariant *icon_data);
int cmp_methods(const void *vkey, const void *velem) int cmp_methods(const void *vkey, const void *velem)
{ {
const char *key = (const char*)vkey; const char *key = (const char*)vkey;
@ -241,7 +239,7 @@ static struct notification *dbus_message_to_notification(const gchar *sender, GV
if (!dict_value) if (!dict_value)
dict_value = g_variant_lookup_value(hints, "icon_data", G_VARIANT_TYPE("(iiibiiay)")); dict_value = g_variant_lookup_value(hints, "icon_data", G_VARIANT_TYPE("(iiibiiay)"));
if (dict_value) { if (dict_value) {
n->raw_icon = get_raw_image_from_data_hint(dict_value); notification_icon_replace_data(n, dict_value);
g_variant_unref(dict_value); g_variant_unref(dict_value);
} }
@ -577,42 +575,6 @@ static void dbus_cb_name_lost(GDBusConnection *connection,
exit(1); exit(1);
} }
static struct raw_image *get_raw_image_from_data_hint(GVariant *icon_data)
{
struct raw_image *image = g_malloc(sizeof(struct raw_image));
GVariant *data_variant;
gsize expected_len;
g_variant_get(icon_data,
"(iiibii@ay)",
&image->width,
&image->height,
&image->rowstride,
&image->has_alpha,
&image->bits_per_sample,
&image->n_channels,
&data_variant);
expected_len = (image->height - 1) * image->rowstride + image->width
* ((image->n_channels * image->bits_per_sample + 7) / 8);
if (expected_len != g_variant_get_size (data_variant)) {
LOG_W("Expected image data to be of length %" G_GSIZE_FORMAT
" but got a length of %" G_GSIZE_FORMAT,
expected_len,
g_variant_get_size(data_variant));
g_free(image);
g_variant_unref(data_variant);
return NULL;
}
image->data = (guchar *) g_memdup(g_variant_get_data(data_variant),
g_variant_get_size(data_variant));
g_variant_unref(data_variant);
return image;
}
int dbus_init(void) int dbus_init(void)
{ {
guint owner_id; guint owner_id;

View File

@ -205,19 +205,120 @@ GdkPixbuf *get_pixbuf_from_icon(const char *iconname)
return pixbuf; return pixbuf;
} }
GdkPixbuf *get_pixbuf_from_raw_image(const struct raw_image *raw_image) GdkPixbuf *icon_get_for_name(const char *name, char **id)
{ {
GdkPixbuf *pixbuf = NULL; ASSERT_OR_RET(name, NULL);
ASSERT_OR_RET(id, NULL);
pixbuf = gdk_pixbuf_new_from_data(raw_image->data, GdkPixbuf *pb = get_pixbuf_from_icon(name);
if (pb)
*id = g_strdup(name);
return pb;
}
GdkPixbuf *icon_get_for_data(GVariant *data, char **id)
{
ASSERT_OR_RET(data, NULL);
ASSERT_OR_RET(id, NULL);
if (!STR_EQ("(iiibiiay)", g_variant_get_type_string(data))) {
LOG_W("Invalid data for pixbuf given.");
return NULL;
}
/* The raw image is a big array of char data.
*
* The image is serialised rowwise pixel by pixel. The rows are aligned
* by a spacer full of garbage. The overall data length of data + garbage
* is called the rowstride.
*
* Mind the missing spacer at the last row.
*
* len: |<--------------rowstride---------------->|
* len: |<-width*pixelstride->|
* row 1: | data for row 1 | spacer of garbage |
* row 2: | data for row 2 | spacer of garbage |
* | . | spacer of garbage |
* | . | spacer of garbage |
* | . | spacer of garbage |
* row n-1: | data for row n-1 | spacer of garbage |
* row n: | data for row n |
*/
GdkPixbuf *pixbuf = NULL;
GVariant *data_variant = NULL;
unsigned char *data_pb;
gsize len_expected;
gsize len_actual;
gsize pixelstride;
int width;
int height;
int rowstride;
int has_alpha;
int bits_per_sample;
int n_channels;
g_variant_get(data,
"(iiibii@ay)",
&width,
&height,
&rowstride,
&has_alpha,
&bits_per_sample,
&n_channels,
&data_variant);
// note: (A+7)/8 rounds up A to the next byte boundary
pixelstride = (n_channels * bits_per_sample + 7)/8;
len_expected = (height - 1) * rowstride + width * pixelstride;
len_actual = g_variant_get_size(data_variant);
if (len_actual != len_expected) {
LOG_W("Expected image data to be of length %" G_GSIZE_FORMAT
" but got a length of %" G_GSIZE_FORMAT,
len_expected,
len_actual);
g_variant_unref(data_variant);
return NULL;
}
data_pb = (guchar *) g_memdup(g_variant_get_data(data_variant), len_actual);
pixbuf = gdk_pixbuf_new_from_data(data_pb,
GDK_COLORSPACE_RGB, GDK_COLORSPACE_RGB,
raw_image->has_alpha, has_alpha,
raw_image->bits_per_sample, bits_per_sample,
raw_image->width, width,
raw_image->height, height,
raw_image->rowstride, rowstride,
NULL, (GdkPixbufDestroyNotify) g_free,
NULL); data_pb);
if (!pixbuf) {
/* Dear user, I'm sorry, I'd like to give you a more specific
* error message. But sadly, I can't */
LOG_W("Cannot serialise raw icon data into pixbuf.");
return NULL;
}
/* To calculate a checksum of the current image, we have to remove
* all excess spacers, so that our checksummed memory only contains
* real data. */
size_t data_chk_len = pixelstride * width * height;
unsigned char *data_chk = g_malloc(data_chk_len);
size_t rowstride_short = pixelstride * width;
for (int i = 0; i < height; i++) {
memcpy(data_chk + (i*rowstride_short),
data_pb + (i*rowstride),
rowstride_short);
}
*id = g_compute_checksum_for_data(G_CHECKSUM_MD5, data_chk, data_chk_len);
g_free(data_chk);
g_variant_unref(data_variant);
return pixbuf; return pixbuf;
} }

View File

@ -37,9 +37,34 @@ GdkPixbuf *get_pixbuf_from_file(const char *filename);
*/ */
GdkPixbuf *get_pixbuf_from_icon(const char *iconname); GdkPixbuf *get_pixbuf_from_icon(const char *iconname);
/** Convert a struct raw_image to a `GdkPixbuf` /** Read an icon from disk and convert it to a GdkPixbuf.
*
* The returned id will be a unique identifier. To check if two given
* GdkPixbufs are equal, it's sufficient to just compare the id strings.
*
* @param name A string describing and icon. May be a full path, a file path or
* just a simple name. If it's a name without a slash, the icon will
* get searched in the folders of the icon_path setting.
* @param id (necessary) A unique identifier of the returned pixbuf. Only filled,
* if the return value is non-NULL.
* @return a pixbuf representing name's image.
* If an invalid path given, it will return NULL.
*/ */
GdkPixbuf *get_pixbuf_from_raw_image(const struct raw_image *raw_image); GdkPixbuf *icon_get_for_name(const char *name, char **id);
/** Convert a GVariant like described in GdkPixbuf
*
* The returned id will be a unique identifier. To check if two given
* GdkPixbufs are equal, it's sufficient to just compare the id strings.
*
* @param data A GVariant in the format "(iiibii@ay)" filled with values
* like described in the notification spec.
* @param id (necessary) A unique identifier of the returned pixbuf.
* Only filled, if the return value is non-NULL.
* @return a pixbuf representing name's image.
* If an invalid GVariant is passed, it will return NULL.
*/
GdkPixbuf *icon_get_for_data(GVariant *data, char **id);
#endif #endif
/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ /* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */

View File

@ -24,7 +24,6 @@
#include "settings.h" #include "settings.h"
#include "utils.h" #include "utils.h"
static void notification_update_icon(struct notification *n);
static void notification_extract_urls(struct notification *n); static void notification_extract_urls(struct notification *n);
static void notification_format_message(struct notification *n); static void notification_format_message(struct notification *n);
@ -55,7 +54,8 @@ void notification_print(const struct notification *n)
printf("\tsummary: '%s'\n", n->summary); printf("\tsummary: '%s'\n", n->summary);
printf("\tbody: '%s'\n", n->body); printf("\tbody: '%s'\n", n->body);
printf("\ticon: '%s'\n", n->iconname); printf("\ticon: '%s'\n", n->iconname);
printf("\traw_icon set: %s\n", (n->raw_icon ? "true" : "false")); printf("\traw_icon set: %s\n", (n->icon_id && !STR_EQ(n->iconname, n->icon_id)) ? "true" : "false");
printf("\ticon_id: '%s'\n", n->icon_id);
printf("\tcategory: %s\n", n->category); printf("\tcategory: %s\n", n->category);
printf("\ttimeout: %ld\n", n->timeout/1000); printf("\ttimeout: %ld\n", n->timeout/1000);
printf("\turgency: %s\n", notification_urgency_to_string(n->urgency)); printf("\turgency: %s\n", notification_urgency_to_string(n->urgency));
@ -175,29 +175,15 @@ int notification_cmp_data(const void *va, const void *vb, void *data)
return notification_cmp(a, b); return notification_cmp(a, b);
} }
int notification_is_duplicate(const struct notification *a, const struct notification *b) bool notification_is_duplicate(const struct notification *a, const struct notification *b)
{ {
//Comparing raw icons is not supported, assume they are not identical
if (settings.icon_position != ICON_OFF
&& (a->raw_icon || b->raw_icon))
return false;
return STR_EQ(a->appname, b->appname) return STR_EQ(a->appname, b->appname)
&& STR_EQ(a->summary, b->summary) && STR_EQ(a->summary, b->summary)
&& STR_EQ(a->body, b->body) && STR_EQ(a->body, b->body)
&& (settings.icon_position != ICON_OFF ? STR_EQ(a->iconname, b->iconname) : 1) && (settings.icon_position != ICON_OFF ? STR_EQ(a->icon_id, b->icon_id) : 1)
&& a->urgency == b->urgency; && a->urgency == b->urgency;
} }
/* see notification.h */
void rawimage_free(struct raw_image *i)
{
ASSERT_OR_RET(i,);
g_free(i->data);
g_free(i);
}
static void notification_private_free(NotificationPrivate *p) static void notification_private_free(NotificationPrivate *p)
{ {
g_free(p); g_free(p);
@ -241,13 +227,44 @@ void notification_unref(struct notification *n)
g_free(n->stack_tag); g_free(n->stack_tag);
g_hash_table_unref(n->actions); g_hash_table_unref(n->actions);
rawimage_free(n->raw_icon);
if (n->icon)
g_object_unref(n->icon);
g_free(n->icon_id);
notification_private_free(n->priv); notification_private_free(n->priv);
g_free(n); g_free(n);
} }
void notification_icon_replace_path(struct notification *n, const char *new_icon)
{
ASSERT_OR_RET(n,);
ASSERT_OR_RET(new_icon,);
ASSERT_OR_RET(n->iconname != new_icon,);
g_free(n->iconname);
n->iconname = g_strdup(new_icon);
g_clear_object(&n->icon);
g_clear_pointer(&n->icon_id, g_free);
n->icon = icon_get_for_name(new_icon, &n->icon_id);
n->icon = icon_pixbuf_scale(n->icon);
}
void notification_icon_replace_data(struct notification *n, GVariant *new_icon)
{
ASSERT_OR_RET(n,);
ASSERT_OR_RET(new_icon,);
g_clear_object(&n->icon);
g_clear_pointer(&n->icon_id, g_free);
n->icon = icon_get_for_data(new_icon, &n->icon_id);
n->icon = icon_pixbuf_scale(n->icon);
}
/* see notification.h */ /* see notification.h */
void notification_replace_single_field(char **haystack, void notification_replace_single_field(char **haystack,
char **needle, char **needle,
@ -332,8 +349,13 @@ void notification_init(struct notification *n)
/* Icon handling */ /* Icon handling */
if (STR_EMPTY(n->iconname)) if (STR_EMPTY(n->iconname))
g_clear_pointer(&n->iconname, g_free); g_clear_pointer(&n->iconname, g_free);
if (!n->raw_icon && !n->iconname) if (!n->icon && n->iconname) {
n->iconname = g_strdup(settings.icons[n->urgency]); char *icon = g_strdup(n->iconname);
notification_icon_replace_path(n, icon);
g_free(icon);
}
if (!n->icon && !n->iconname)
notification_icon_replace_path(n, settings.icons[n->urgency]);
/* Color hints */ /* Color hints */
struct notification_colors defcolors; struct notification_colors defcolors;
@ -365,25 +387,10 @@ void notification_init(struct notification *n)
rule_apply_all(n); rule_apply_all(n);
/* UPDATE derived fields */ /* UPDATE derived fields */
notification_update_icon(n);
notification_extract_urls(n); notification_extract_urls(n);
notification_format_message(n); notification_format_message(n);
} }
static void notification_update_icon(struct notification *n)
{
g_return_if_fail(n);
g_clear_object(&n->icon);
if (n->raw_icon)
n->icon = get_pixbuf_from_raw_image(n->raw_icon);
else if (n->iconname)
n->icon = get_pixbuf_from_icon(n->iconname);
n->icon = icon_pixbuf_scale(n->icon);
}
static void notification_format_message(struct notification *n) static void notification_format_message(struct notification *n)
{ {
g_clear_pointer(&n->msg, g_free); g_clear_pointer(&n->msg, g_free);

View File

@ -27,16 +27,6 @@ enum urgency {
URG_MAX = 2, /**< Maximum value, useful for boundary checking */ URG_MAX = 2, /**< Maximum value, useful for boundary checking */
}; };
struct raw_image {
int width;
int height;
int rowstride;
int has_alpha;
int bits_per_sample;
int n_channels;
unsigned char *data;
};
typedef struct _notification_private NotificationPrivate; typedef struct _notification_private NotificationPrivate;
struct notification_colors { struct notification_colors {
@ -57,9 +47,11 @@ struct notification {
char *category; char *category;
enum urgency urgency; enum urgency urgency;
GdkPixbuf *icon; GdkPixbuf *icon; /**< The raw cached icon data used to draw */
char *iconname; /**< plain icon information (may be a path or just a name) */ char *icon_id; /**< plain icon information, which acts as the pixbuf's id, which is saved in .icon
struct raw_image *raw_icon; /**< passed icon data of notification, takes precedence over icon */ May be a hash for a raw icon or a name/path for a regular app icon. */
char *iconname; /**< plain icon information (may be a path or just a name)
Use this to compare the icon name with rules.*/
gint64 start; /**< begin of current display */ gint64 start; /**< begin of current display */
gint64 timestamp; /**< arrival time */ gint64 timestamp; /**< arrival time */
@ -123,13 +115,6 @@ void notification_ref(struct notification *n);
*/ */
void notification_init(struct notification *n); void notification_init(struct notification *n);
/**
* Free a #raw_image
*
* @param i (nullable): pointer to #raw_image
*/
void rawimage_free(struct raw_image *i);
/** /**
* Decrease the reference counter of the notification. * Decrease the reference counter of the notification.
* *
@ -148,7 +133,26 @@ int notification_cmp(const struct notification *a, const struct notification *b)
*/ */
int notification_cmp_data(const void *va, const void *vb, void *data); int notification_cmp_data(const void *va, const void *vb, void *data);
int notification_is_duplicate(const struct notification *a, const struct notification *b); bool notification_is_duplicate(const struct notification *a, const struct notification *b);
/**Replace the current notification's icon with the icon specified by path.
*
* Removes the reference for the previous icon automatically and will also free the
* iconname field. So passing n->iconname as new_icon is invalid.
*
* @param n the notification to replace the icon
* @param new_icon The path of the new icon. May be an absolute path or an icon name.
*/
void notification_icon_replace_path(struct notification *n, const char *new_icon);
/**Replace the current notification's icon with the raw icon given in the GVariant.
*
* Removes the reference for the previous icon automatically.
*
* @param n the notification to replace the icon
* @param new_icon The icon's data. Has to be in the format of the notification spec.
*/
void notification_icon_replace_data(struct notification *n, GVariant *new_icon);
/** /**
* Run the script associated with the * Run the script associated with the

View File

@ -26,11 +26,8 @@ void rule_apply(struct rule *r, struct notification *n)
n->transient = r->set_transient; n->transient = r->set_transient;
if (r->markup != MARKUP_NULL) if (r->markup != MARKUP_NULL)
n->markup = r->markup; n->markup = r->markup;
if (r->new_icon) { if (r->new_icon)
g_free(n->iconname); notification_icon_replace_path(n, r->new_icon);
n->iconname = g_strdup(r->new_icon);
g_clear_pointer(&n->raw_icon, rawimage_free);
}
if (r->fg) { if (r->fg) {
g_free(n->colors.fg); g_free(n->colors.fg);
n->colors.fg = g_strdup(r->fg); n->colors.fg = g_strdup(r->fg);

View File

@ -618,7 +618,8 @@ TEST test_hint_raw_image(void)
ASSERT_EQ(queues_length_waiting(), len+1); ASSERT_EQ(queues_length_waiting(), len+1);
n = queues_debug_find_notification_by_id(id); n = queues_debug_find_notification_by_id(id);
ASSERT(n->raw_icon); ASSERT(n->icon);
ASSERT(!STR_EQ(n->icon_id, n_dbus->app_icon));
dbus_notification_free(n_dbus); dbus_notification_free(n_dbus);
g_free(path); g_free(path);

View File

@ -26,6 +26,7 @@ TEST test_notification_is_duplicate(void)
a->summary = g_strdup("Summary"); a->summary = g_strdup("Summary");
a->body = g_strdup("Body"); a->body = g_strdup("Body");
a->iconname = g_strdup("Icon"); a->iconname = g_strdup("Icon");
a->icon_id = g_strdup("Icon");
a->urgency = URG_NORM; a->urgency = URG_NORM;
struct notification *b = notification_create(); struct notification *b = notification_create();
@ -33,8 +34,11 @@ TEST test_notification_is_duplicate(void)
b->summary = g_strdup("Summary"); b->summary = g_strdup("Summary");
b->body = g_strdup("Body"); b->body = g_strdup("Body");
b->iconname = g_strdup("Icon"); b->iconname = g_strdup("Icon");
b->icon_id = g_strdup("Icon");
b->urgency = URG_NORM; b->urgency = URG_NORM;
ASSERT(notification_is_duplicate(a, b));
CHECK_CALL(test_notification_is_duplicate_field(&(b->appname), a, b)); CHECK_CALL(test_notification_is_duplicate_field(&(b->appname), a, b));
CHECK_CALL(test_notification_is_duplicate_field(&(b->summary), a, b)); CHECK_CALL(test_notification_is_duplicate_field(&(b->summary), a, b));
CHECK_CALL(test_notification_is_duplicate_field(&(b->body), a, b)); CHECK_CALL(test_notification_is_duplicate_field(&(b->body), a, b));
@ -47,21 +51,17 @@ TEST test_notification_is_duplicate(void)
settings.icon_position = ICON_OFF; settings.icon_position = ICON_OFF;
ASSERT(notification_is_duplicate(a, b)); ASSERT(notification_is_duplicate(a, b));
//Setting pointer to a random value since we are checking for null //Setting pointer to a random value since we are checking for null
b->raw_icon = (struct raw_image*)0xff; char *icon_id = b->icon_id;
ASSERT(notification_is_duplicate(a, b)); b->icon_id = "false";
b->raw_icon = NULL; ASSERTm("Icons have to get ignored for duplicate check when icons are off",
notification_is_duplicate(a, b));
b->icon_id = icon_id;
settings.icon_position = ICON_LEFT; settings.icon_position = ICON_LEFT;
CHECK_CALL(test_notification_is_duplicate_field(&(b->iconname), a, b)); CHECK_CALL(test_notification_is_duplicate_field(&(b->icon_id), a, b));
b->raw_icon = (struct raw_image*)0xff;
ASSERT_FALSE(notification_is_duplicate(a, b));
b->raw_icon = NULL;
settings.icon_position = ICON_RIGHT; settings.icon_position = ICON_RIGHT;
CHECK_CALL(test_notification_is_duplicate_field(&(b->iconname), a, b)); CHECK_CALL(test_notification_is_duplicate_field(&(b->icon_id), a, b));
b->raw_icon = (struct raw_image*)0xff;
ASSERT_FALSE(notification_is_duplicate(a, b));
b->raw_icon = NULL;
settings.icon_position = icon_setting_tmp; settings.icon_position = icon_setting_tmp;