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, &notif->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