1 #define _POSIX_C_SOURCE 200809L
2 #include <assert.h>
3 #include <errno.h>
4 #include <fnmatch.h>
5 #include <glob.h>
6 #include <libgen.h>
7 #include <stdio.h>
8 #include <stdlib.h>
9 #include <assert.h>
10 #include <ctype.h>
11 #include <cairo/cairo.h>
12
13 #include "mako.h"
14 #include "icon.h"
15 #include "string-util.h"
16 #include "wayland.h"
17
18 #ifdef HAVE_ICONS
19
20 #include <gdk-pixbuf/gdk-pixbuf.h>
21 #include "cairo-pixbuf.h"
22
load_image(const char * path)23 static GdkPixbuf *load_image(const char *path) {
24 if (strlen(path) == 0) {
25 return NULL;
26 }
27 GError *err = NULL;
28 GdkPixbuf *pixbuf = gdk_pixbuf_new_from_file(path, &err);
29 if (!pixbuf) {
30 fprintf(stderr, "Failed to load icon (%s)\n", err->message);
31 g_error_free(err);
32 return NULL;
33 }
34 return pixbuf;
35 }
36
load_image_data(struct mako_image_data * image_data)37 static GdkPixbuf *load_image_data(struct mako_image_data *image_data) {
38 GdkPixbuf *pixbuf = gdk_pixbuf_new_from_data(image_data->data, GDK_COLORSPACE_RGB,
39 image_data->has_alpha, image_data->bits_per_sample, image_data->width,
40 image_data->height, image_data->rowstride, NULL, NULL);
41 if (!pixbuf) {
42 fprintf(stderr, "Failed to load icon\n");
43 return NULL;
44 }
45 return pixbuf;
46 }
47
fit_to_square(int width,int height,int square_size)48 static double fit_to_square(int width, int height, int square_size) {
49 double longest = width > height ? width : height;
50 return longest > square_size ? square_size/longest : 1.0;
51 }
52
hex_val(char digit)53 static char hex_val(char digit) {
54 assert(isxdigit(digit));
55 if (digit >= 'a') {
56 return digit - 'a' + 10;
57 } else if (digit >= 'A') {
58 return digit - 'A' + 10;
59 } else {
60 return digit - '0';
61 }
62 }
63
url_decode(char * dst,const char * src)64 static void url_decode(char *dst, const char *src) {
65 while (src[0]) {
66 if (src[0] == '%' && isxdigit(src[1]) && isxdigit(src[2])) {
67 dst[0] = 16*hex_val(src[1]) + hex_val(src[2]);
68 dst++; src += 3;
69 } else {
70 dst[0] = src[0];
71 dst++; src++;
72 }
73 }
74 dst[0] = '\0';
75 }
76
77 // Attempt to find a full path for a notification's icon_name, which may be:
78 // - An absolute path, which will simply be returned (as a new string)
79 // - A file:// URI, which will be converted to an absolute path
80 // - A Freedesktop icon name, which will be resolved within the configured
81 // `icon-path` using something that looks vaguely like the algorithm defined
82 // in the icon theme spec (https://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html)
83 //
84 // Returns the resolved path, or NULL if it was unable to find an icon. The
85 // return value must be freed by the caller.
resolve_icon(struct mako_notification * notif)86 static char *resolve_icon(struct mako_notification *notif) {
87 char *icon_name = notif->app_icon;
88 if (icon_name[0] == '\0') {
89 return NULL;
90 }
91 if (icon_name[0] == '/') {
92 return strdup(icon_name);
93 }
94 if (strstr(icon_name, "file://") == icon_name) {
95 // Chop off the scheme and URL decode
96 char *icon_path = malloc(strlen(icon_name) + 1 - strlen("file://"));
97 if (icon_path == NULL) {
98 return icon_path;
99 }
100
101 url_decode(icon_path, icon_name + strlen("file://"));
102 return icon_path;
103 }
104
105 // Determine the largest scale factor of any attached output.
106 int32_t max_scale = 1;
107 struct mako_output *output = NULL;
108 wl_list_for_each(output, ¬if->state->outputs, link) {
109 if (output->scale > max_scale) {
110 max_scale = output->scale;
111 }
112 }
113
114 static const char fallback[] = "%s:/usr/local/share/icons/hicolor";
115 char *search = mako_asprintf(fallback, notif->style.icon_path);
116
117 char *saveptr = NULL;
118 char *theme_path = strtok_r(search, ":", &saveptr);
119
120 // Match all icon files underneath of the theme_path followed by any icon
121 // size and category subdirectories. This pattern assumes that all the
122 // files in the icon path are valid icon types.
123 static const char pattern_fmt[] = "%s/*/*/%s.*";
124
125 char *icon_path = NULL;
126 int32_t last_icon_size = 0;
127 while (theme_path) {
128 if (strlen(theme_path) == 0) {
129 continue;
130 }
131
132 glob_t icon_glob = {0};
133 char *pattern = mako_asprintf(pattern_fmt, theme_path, icon_name);
134
135 // Disable sorting because we're going to do our own anyway.
136 int found = glob(pattern, GLOB_NOSORT, NULL, &icon_glob);
137 size_t found_count = 0;
138 if (found == 0) {
139 // The value of gl_pathc isn't guaranteed to be usable if glob
140 // returns non-zero.
141 found_count = icon_glob.gl_pathc;
142 }
143
144 for (size_t i = 0; i < found_count; ++i) {
145 char *relative_path = icon_glob.gl_pathv[i];
146
147 // Find the end of the current search path and walk to the next
148 // path component. Hopefully this will be the icon resolution
149 // subdirectory.
150 relative_path += strlen(theme_path);
151 while (relative_path[0] == '/') {
152 ++relative_path;
153 }
154
155 errno = 0;
156 int32_t icon_size = strtol(relative_path, NULL, 10);
157 if (errno || icon_size == 0) {
158 // Try second level subdirectory if failed.
159 errno = 0;
160 while (relative_path[0] != '/') {
161 ++relative_path;
162 }
163 ++relative_path;
164 icon_size = strtol(relative_path, NULL, 10);
165 if (errno || icon_size == 0) {
166 continue;
167 }
168 }
169
170 int32_t icon_scale = 1;
171 char *scale_str = strchr(relative_path, '@');
172 if (scale_str != NULL) {
173 icon_scale = strtol(scale_str + 1, NULL, 10);
174 }
175
176 if (icon_size == notif->style.max_icon_size &&
177 icon_scale == max_scale) {
178 // If we find an exact match, we're done.
179 free(icon_path);
180 icon_path = strdup(icon_glob.gl_pathv[i]);
181 break;
182 } else if (icon_size < notif->style.max_icon_size * max_scale &&
183 icon_size > last_icon_size) {
184 // Otherwise, if this icon is small enough to fit but bigger
185 // than the last best match, choose it on a provisional basis.
186 // We multiply by max_scale to increase the odds of finding an
187 // icon which looks sharp on the highest-scale output.
188 free(icon_path);
189 icon_path = strdup(icon_glob.gl_pathv[i]);
190 last_icon_size = icon_size;
191 }
192 }
193
194 free(pattern);
195 globfree(&icon_glob);
196
197 if (icon_path) {
198 // The spec says that if we find any match whatsoever in a theme,
199 // we should stop there to avoid mixing icons from different
200 // themes even if one is a better size.
201 break;
202 }
203 theme_path = strtok_r(NULL, ":", &saveptr);
204 }
205
206 if (icon_path == NULL) {
207 // Finally, fall back to looking in /usr/local/share/pixmaps. These are
208 // unsized icons, which may lead to downscaling, but some apps are
209 // still using it.
210 static const char pixmaps_fmt[] = "/usr/local/share/pixmaps/%s.*";
211
212 char *pattern = mako_asprintf(pixmaps_fmt, icon_name);
213
214 glob_t icon_glob = {0};
215 int found = glob(pattern, GLOB_NOSORT, NULL, &icon_glob);
216
217 if (found == 0 && icon_glob.gl_pathc > 0) {
218 icon_path = strdup(icon_glob.gl_pathv[0]);
219 }
220 free(pattern);
221 globfree(&icon_glob);
222 }
223
224 free(search);
225 return icon_path;
226 }
227
create_icon(struct mako_notification * notif)228 struct mako_icon *create_icon(struct mako_notification *notif) {
229 GdkPixbuf *image = NULL;
230 if (notif->image_data != NULL) {
231 image = load_image_data(notif->image_data);
232 }
233
234 if (image == NULL) {
235 char *path = resolve_icon(notif);
236 if (path == NULL) {
237 return NULL;
238 }
239
240 image = load_image(path);
241 free(path);
242 if (image == NULL) {
243 return NULL;
244 }
245 }
246
247 int image_width = gdk_pixbuf_get_width(image);
248 int image_height = gdk_pixbuf_get_height(image);
249
250 struct mako_icon *icon = calloc(1, sizeof(struct mako_icon));
251 icon->scale = fit_to_square(
252 image_width, image_height, notif->style.max_icon_size);
253 icon->width = image_width * icon->scale;
254 icon->height = image_height * icon->scale;
255
256 icon->image = create_cairo_surface_from_gdk_pixbuf(image);
257 g_object_unref(image);
258 if (icon->image == NULL) {
259 free(icon);
260 return NULL;
261 }
262
263 return icon;
264 }
265 #else
create_icon(struct mako_notification * notif)266 struct mako_icon *create_icon(struct mako_notification *notif) {
267 return NULL;
268 }
269 #endif
270
draw_icon(cairo_t * cairo,struct mako_icon * icon,double xpos,double ypos,double scale)271 void draw_icon(cairo_t *cairo, struct mako_icon *icon,
272 double xpos, double ypos, double scale) {
273 cairo_save(cairo);
274 cairo_scale(cairo, scale*icon->scale, scale*icon->scale);
275 cairo_set_source_surface(cairo, icon->image, xpos/icon->scale, ypos/icon->scale);
276 cairo_paint(cairo);
277 cairo_restore(cairo);
278 }
279
destroy_icon(struct mako_icon * icon)280 void destroy_icon(struct mako_icon *icon) {
281 if (icon != NULL) {
282 if (icon->image != NULL) {
283 cairo_surface_destroy(icon->image);
284 }
285 free(icon);
286 }
287 }
288