1 /*
2  * Copyright (C) 2013 Bastien Nocera <hadess@hadess.net>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA.
17  *
18  * The Totem project hereby grant permission for non-gpl compatible GStreamer
19  * plugins to be used and distributed together with GStreamer and Totem. This
20  * permission are above and beyond the permissions granted by the GPL license
21  * Totem is covered by.
22  *
23  * Monday 7th February 2005: Christian Schaller: Add exception clause.
24  * See license_change file for details.
25  *
26  */
27 
28 #include <icon-helpers.h>
29 
30 #define GNOME_DESKTOP_USE_UNSTABLE_API 1
31 #include <libgnome-desktop/gnome-desktop-thumbnail.h>
32 
33 #define DEFAULT_MAX_THREADS   1
34 #define THUMB_SEARCH_SIZE     256
35 #define THUMB_SEARCH_HEIGHT   THUMB_SEARCH_SIZE
36 #define SOURCES_MAX_HEIGHT    64
37 #define VIDEO_ICON_SIZE       32
38 
39 typedef enum {
40 	ICON_BOX = 0,
41 	ICON_CHANNEL,
42 	ICON_VIDEO,
43 	ICON_VIDEO_THUMBNAILING,
44 	ICON_OPTICAL,
45 	NUM_ICONS
46 } IconType;
47 
48 static GnomeDesktopThumbnailFactory *factory;
49 static GThreadPool *thumbnail_pool;
50 static GdkPixbuf *icons[NUM_ICONS];
51 static GHashTable *cache_thumbnails; /* key=url, value=GdkPixbuf */
52 
53 #define STROKE           0x3b3c38ff
54 #define FILL_DEFAULT     0x2d2d2dff
55 #define FILL_TRANSPARENT 0x00000000
56 #define FILL_MOVIE       0x000000ff
57 
58 static GdkPixbuf *load_icon (GdkPixbuf *pixbuf,
59 			     gboolean   resize,
60 			     guint32    fill);
61 static GdkPixbuf *load_named_icon (const char *name,
62 				   int         size,
63 				   guint32     fill);
64 
65 static gboolean
media_is_local(GrlMedia * media)66 media_is_local (GrlMedia *media)
67 {
68 	const char *id;
69 
70 	id = grl_media_get_source (media);
71 	if (g_strcmp0 (id, "grl-tracker-source") == 0 ||
72 	    g_strcmp0 (id, "grl-tracker3-source") == 0 ||
73 	    g_strcmp0 (id, "grl-filesystem") == 0 ||
74 	    g_strcmp0 (id, "grl-bookmarks") == 0)
75 		return TRUE;
76 	return FALSE;
77 }
78 
79 GdkPixbuf *
totem_grilo_get_thumbnail_finish(GObject * source_object,GAsyncResult * res,GError ** error)80 totem_grilo_get_thumbnail_finish (GObject       *source_object,
81 				  GAsyncResult  *res,
82 				  GError       **error)
83 {
84 	g_return_val_if_fail (g_task_is_valid (res, source_object), NULL);
85 
86 	return g_task_propagate_pointer (G_TASK (res), error);
87 }
88 
89 static void
load_thumbnail_cb(GObject * source_object,GAsyncResult * res,gpointer user_data)90 load_thumbnail_cb (GObject *source_object,
91 		   GAsyncResult *res,
92 		   gpointer user_data)
93 {
94 	GTask *task = user_data;
95 	GdkPixbuf *pixbuf;
96 	GError *error = NULL;
97 	const GFile *file;
98 
99 	pixbuf = gdk_pixbuf_new_from_stream_finish (res, &error);
100 	if (!pixbuf) {
101 		g_task_return_error (task, error);
102 		g_object_unref (task);
103 		return;
104 	}
105 
106 	/* Cache it */
107 	file = g_task_get_task_data (task);
108 	if (file) {
109 		gboolean is_source;
110 
111 		is_source = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "is-source"));
112 		if (is_source) {
113 			GdkPixbuf *new_pixbuf;
114 
115 			new_pixbuf = load_icon (pixbuf, TRUE, FILL_DEFAULT);
116 			g_object_unref (pixbuf);
117 			pixbuf = new_pixbuf;
118 		} else {
119 			GdkPixbuf *new_pixbuf;
120 
121 			new_pixbuf = load_icon (pixbuf, FALSE, FILL_MOVIE);
122 			g_object_unref (pixbuf);
123 			pixbuf = new_pixbuf;
124 		}
125 		g_hash_table_insert (cache_thumbnails,
126 				     g_file_get_uri (G_FILE (file)),
127 				     g_object_ref (pixbuf));
128 	}
129 
130 	g_task_return_pointer (task, pixbuf, g_object_unref);
131 	g_object_unref (task);
132 }
133 
134 static void
get_stream_thumbnail_cb(GObject * source_object,GAsyncResult * res,gpointer user_data)135 get_stream_thumbnail_cb (GObject *source_object,
136 			 GAsyncResult *res,
137 			 gpointer user_data)
138 {
139 	GTask *task = user_data;
140 	GFileInputStream *stream;
141 	GError *error = NULL;
142 	gboolean is_source;
143 
144 	stream = g_file_read_finish (G_FILE (source_object), res, &error);
145 	if (!stream) {
146 		g_task_return_error (task, error);
147 		g_object_unref (task);
148 		return;
149 	}
150 
151 	is_source = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "is-source"));
152 	gdk_pixbuf_new_from_stream_at_scale_async (G_INPUT_STREAM (stream),
153 						   is_source ? -1 : THUMB_SEARCH_SIZE - 2,
154 						   is_source ? -1 : THUMB_SEARCH_HEIGHT -2 ,
155 						   TRUE,
156 						   g_task_get_cancellable (task),
157 						   load_thumbnail_cb,
158 						   task);
159 	g_object_unref (G_OBJECT (stream));
160 }
161 
162 static void
save_bookmark_thumbnail(GrlMedia * media,const char * uri)163 save_bookmark_thumbnail (GrlMedia *media,
164 			 const char *uri)
165 {
166 	char *thumb_path, *thumb_url;
167 	GrlRegistry *registry;
168 	GrlSource *source;
169 
170 	if (g_strcmp0 (grl_media_get_source (media), "grl-bookmarks") != 0)
171 		return;
172 
173 	thumb_path = gnome_desktop_thumbnail_path_for_uri (uri, GNOME_DESKTOP_THUMBNAIL_SIZE_LARGE);
174 	thumb_url = g_filename_to_uri (thumb_path, NULL, NULL);
175 	g_free (thumb_path);
176 	grl_media_set_thumbnail (media, thumb_url);
177 	g_free (thumb_url);
178 
179 	registry = grl_registry_get_default ();
180 	source = grl_registry_lookup_source (registry, "grl-bookmarks");
181 	grl_source_store_sync (source,
182 			       NULL,
183 			       media,
184 			       GRL_WRITE_NORMAL,
185 			       NULL);
186 }
187 
188 static void
thumbnail_media_async_thread(GTask * task,gpointer user_data)189 thumbnail_media_async_thread (GTask    *task,
190 			      gpointer  user_data)
191 {
192 	GrlMedia *media;
193 	GdkPixbuf *pixbuf, *tmp_pixbuf;
194 	const char *uri;
195 	GDateTime *mtime;
196 	gint64 unix_date;
197 
198 	if (g_task_return_error_if_cancelled (task)) {
199 		g_object_unref (task);
200 		return;
201 	}
202 
203 	media = GRL_MEDIA (g_task_get_source_object (task));
204 	uri = grl_media_get_url (media);
205 
206 	mtime = grl_media_get_modification_date (media);
207 	if (!mtime) {
208 		GrlRegistry *registry;
209 		GrlKeyID key_id;
210 
211 		registry = grl_registry_get_default ();
212 		key_id = grl_registry_lookup_metadata_key (registry, "bookmark-date");
213 		mtime = grl_data_get_boxed (GRL_DATA (media), key_id);
214 	}
215 	unix_date = mtime ? g_date_time_to_unix (mtime) : g_get_real_time () / 1000000;
216 
217 	if (!uri) {
218 		g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "URI missing");
219 		g_object_unref (task);
220 		return;
221 	}
222 
223 	tmp_pixbuf = gnome_desktop_thumbnail_factory_generate_thumbnail (factory, uri, "video/x-totem-stream");
224 
225 	if (!tmp_pixbuf) {
226 		g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "Thumbnailing failed");
227 		g_object_unref (task);
228 		return;
229 	}
230 
231 	gnome_desktop_thumbnail_factory_save_thumbnail (factory, tmp_pixbuf, uri, unix_date);
232 
233 	/* Save the thumbnail URL for the bookmarks source */
234 	save_bookmark_thumbnail (media, uri);
235 
236 	/* Add frame */
237 	pixbuf = load_icon (tmp_pixbuf, FALSE, FILL_MOVIE);
238 	g_object_unref (tmp_pixbuf);
239 
240 	g_task_return_pointer (task, pixbuf, g_object_unref);
241 	g_object_unref (task);
242 }
243 
244 static void
totem_grilo_thumbnail_media(GrlMedia * media,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)245 totem_grilo_thumbnail_media (GrlMedia            *media,
246 			     GCancellable        *cancellable,
247 			     GAsyncReadyCallback  callback,
248 			     gpointer             user_data)
249 {
250 	GTask *task;
251 
252 	task = g_task_new (media, cancellable, callback, user_data);
253 	g_task_set_priority (task, G_PRIORITY_LOW);
254 	g_thread_pool_push (thumbnail_pool, task, NULL);
255 }
256 
257 static GdkPixbuf *
totem_grilo_thumbnail_media_finish(GrlMedia * media,GAsyncResult * res,GError ** error)258 totem_grilo_thumbnail_media_finish (GrlMedia      *media,
259 				    GAsyncResult  *res,
260 				    GError       **error)
261 {
262 	g_return_val_if_fail (g_task_is_valid (res, media), NULL);
263 
264 	return g_task_propagate_pointer (G_TASK (res), error);
265 }
266 
267 static void
thumbnail_media_cb(GObject * source_object,GAsyncResult * res,gpointer user_data)268 thumbnail_media_cb (GObject      *source_object,
269 		    GAsyncResult *res,
270 		    gpointer      user_data)
271 {
272 	GTask *task = user_data;
273 	GdkPixbuf *pixbuf;
274 	GError *error = NULL;
275 
276 	pixbuf = totem_grilo_thumbnail_media_finish (GRL_MEDIA (source_object), res, &error);
277 	if (!pixbuf)
278 		g_task_return_error (task, error);
279 	else
280 		g_task_return_pointer (task, pixbuf, g_object_unref);
281 	g_object_unref (task);
282 }
283 
284 void
totem_grilo_get_thumbnail(GObject * object,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)285 totem_grilo_get_thumbnail (GObject             *object,
286 			   GCancellable        *cancellable,
287 			   GAsyncReadyCallback  callback,
288 			   gpointer             user_data)
289 {
290 	GTask *task;
291 	const char *url_thumb = NULL;
292 	const GdkPixbuf *thumbnail = NULL;
293 	GFile *file;
294 
295 	task = g_task_new (G_OBJECT (object),
296 			   cancellable,
297 			   callback,
298 			   user_data);
299 
300 	if (GRL_IS_MEDIA (object)) {
301 		url_thumb = grl_media_get_thumbnail (GRL_MEDIA (object));
302 		if (!url_thumb && media_is_local (GRL_MEDIA (object))) {
303 			totem_grilo_thumbnail_media (GRL_MEDIA (object),
304 						     cancellable,
305 						     thumbnail_media_cb,
306 						     task);
307 			return;
308 		}
309 	} else if (GRL_IS_SOURCE (object)) {
310 		GIcon *icon;
311 
312 		icon = grl_source_get_icon (GRL_SOURCE (object));
313 		if (icon) {
314 			GFile *file;
315 
316 			file = g_file_icon_get_file (G_FILE_ICON (icon));
317 			url_thumb = g_file_get_uri (file);
318 
319 			g_object_set_data (G_OBJECT (task), "is-source", GUINT_TO_POINTER (TRUE));
320 		}
321 	}
322 	if (url_thumb == NULL) {
323 		g_task_return_pointer (task, NULL, NULL);
324 		g_object_unref (task);
325 		return;
326 	}
327 
328 	/* Check cache */
329 	thumbnail = g_hash_table_lookup (cache_thumbnails, url_thumb);
330 	if (thumbnail) {
331 		g_task_return_pointer (task,
332 				       g_object_ref (G_OBJECT (thumbnail)),
333 				       g_object_unref);
334 		g_object_unref (task);
335 		return;
336 	}
337 
338 	file = g_file_new_for_uri (url_thumb);
339 	g_task_set_task_data (task, file, g_object_unref);
340 	g_file_read_async (file, G_PRIORITY_DEFAULT, cancellable,
341 			   get_stream_thumbnail_cb, task);
342 }
343 
344 static void
put_pixel(guchar * p)345 put_pixel (guchar *p)
346 {
347 	p[0] = (STROKE & 0xff000000) >> 24;
348 	p[1] = (STROKE & 0x00ff0000) >> 16;
349 	p[2] = (STROKE & 0x0000ff00) >> 8;
350 	p[3] = (STROKE & 0x000000ff);
351 }
352 
353 static GdkPixbuf *
load_icon(GdkPixbuf * pixbuf,gboolean resize,guint32 fill)354 load_icon (GdkPixbuf  *pixbuf,
355 	   gboolean    resize,
356 	   guint32     fill)
357 {
358 	GdkPixbuf *ret;
359 	guchar *pixels;
360 	int rowstride;
361 	int x, y;
362 	int width, height;
363 	gdouble offset_x, offset_y, scale;
364 	int dest_x, dest_y;
365 
366 	ret = gdk_pixbuf_new (GDK_COLORSPACE_RGB,
367 			      TRUE,
368 			      8, THUMB_SEARCH_SIZE, THUMB_SEARCH_HEIGHT);
369 	pixels = gdk_pixbuf_get_pixels (ret);
370 	rowstride = gdk_pixbuf_get_rowstride (ret);
371 
372 	/* Clean up and draw a border */
373 	gdk_pixbuf_fill (ret, fill);
374 
375 	/* top */
376 	for (x = 0; x < THUMB_SEARCH_SIZE; x++)
377 		put_pixel (pixels + x * 4);
378 	/* bottom */
379 	for (x = 0; x < THUMB_SEARCH_SIZE; x++)
380 		put_pixel (pixels + (THUMB_SEARCH_HEIGHT -1) * rowstride + x * 4);
381 	/* left */
382 	for (y = 1; y < THUMB_SEARCH_HEIGHT - 1; y++)
383 		put_pixel (pixels + y * rowstride);
384 	/* right */
385 	for (y = 1; y < THUMB_SEARCH_HEIGHT - 1; y++)
386 		put_pixel (pixels + y * rowstride + (THUMB_SEARCH_SIZE - 1) * 4);
387 
388 	width = gdk_pixbuf_get_width (pixbuf);
389 	height = gdk_pixbuf_get_height (pixbuf);
390 
391 	if (resize &&
392 	    (width > (THUMB_SEARCH_SIZE / 4 * 3) ||
393 	     height > SOURCES_MAX_HEIGHT)) {
394 		gdouble scale_x, scale_y;
395 
396 		scale_x = ((gdouble) THUMB_SEARCH_SIZE / 4.0 * 3.0) / (gdouble) width;
397 		scale_y = (gdouble) SOURCES_MAX_HEIGHT / (gdouble) height;
398 
399 		scale = MIN(MIN(scale_x, scale_y), 1.0);
400 	} else {
401 		scale = 1.0;
402 	}
403 
404 	/* Put the icon in the middle, with help from this post:
405 	 * http://permalink.gmane.org/gmane.comp.desktop.rox.devel/9065 */
406 	offset_x = (THUMB_SEARCH_SIZE - width * scale) / 2;
407 	offset_y = (THUMB_SEARCH_HEIGHT - height * scale) / 2;
408 	dest_x = MAX(offset_x, 0);
409 	dest_y = MAX(offset_y, 0);
410 	gdk_pixbuf_composite (pixbuf, ret,
411 			      dest_x, dest_y,
412 			      MIN(THUMB_SEARCH_SIZE, width * scale),
413 			      MIN(THUMB_SEARCH_HEIGHT, height * scale),
414 			      offset_x,
415 			      offset_y,
416 			      scale, scale,
417 			      GDK_INTERP_BILINEAR,
418 			      255);
419 
420 	return ret;
421 }
422 
423 static GdkPixbuf *
load_named_icon(const char * name,int size,guint32 fill)424 load_named_icon (const char *name,
425 		 int         size,
426 		 guint32     fill)
427 {
428 	GdkScreen *screen;
429 	GIcon *icon;
430 	GList *windows;
431 	GtkIconInfo *info;
432 	GtkIconTheme *theme;
433 	GtkStyleContext *context;
434 	GdkPixbuf *pixbuf, *ret;
435 
436 	windows = gtk_window_list_toplevels ();
437 	if (windows == NULL)
438 		return NULL;
439 
440 	icon = g_themed_icon_new (name);
441 	screen = gdk_screen_get_default ();
442 	theme = gtk_icon_theme_get_for_screen (screen);
443 	info = gtk_icon_theme_lookup_by_gicon (theme, icon, size, GTK_ICON_LOOKUP_FORCE_SYMBOLIC);
444 	context = gtk_widget_get_style_context (GTK_WIDGET (windows->data));
445 	pixbuf = gtk_icon_info_load_symbolic_for_context (info, context, NULL, NULL);
446 
447 	ret = load_icon (pixbuf, FALSE, fill);
448 
449 	g_object_unref (pixbuf);
450 	g_object_unref (info);
451 	g_object_unref (icon);
452 
453 	return ret;
454 }
455 
456 GdkPixbuf *
totem_grilo_get_icon(GrlMedia * media,gboolean * thumbnailing)457 totem_grilo_get_icon (GrlMedia *media,
458 		      gboolean *thumbnailing)
459 {
460 	g_return_val_if_fail (thumbnailing != NULL, NULL);
461 
462 	*thumbnailing = FALSE;
463 
464 	if (grl_media_is_container (media)) {
465 		return g_object_ref (icons[ICON_BOX]);
466 	} else {
467 		if (grl_media_get_thumbnail (media) ||
468 		    media_is_local (media)) {
469 			*thumbnailing = TRUE;
470 			return g_object_ref (icons[ICON_VIDEO_THUMBNAILING]);
471 		} else {
472 			if (g_str_equal (grl_media_get_source (media), "grl-optical-media"))
473 				return g_object_ref (icons[ICON_OPTICAL]);
474 			return g_object_ref (icons[ICON_VIDEO]);
475 		}
476 	}
477 	return NULL;
478 }
479 
480 const GdkPixbuf *
totem_grilo_get_video_icon(void)481 totem_grilo_get_video_icon (void)
482 {
483 	return icons[ICON_VIDEO];
484 }
485 
486 const GdkPixbuf *
totem_grilo_get_box_icon(void)487 totem_grilo_get_box_icon (void)
488 {
489 	return icons[ICON_BOX];
490 }
491 
492 const GdkPixbuf *
totem_grilo_get_channel_icon(void)493 totem_grilo_get_channel_icon (void)
494 {
495 	return icons[ICON_CHANNEL];
496 }
497 
498 const GdkPixbuf *
totem_grilo_get_optical_icon(void)499 totem_grilo_get_optical_icon (void)
500 {
501 	return icons[ICON_OPTICAL];
502 }
503 
504 void
totem_grilo_clear_icons(void)505 totem_grilo_clear_icons (void)
506 {
507 	guint i;
508 
509 	for (i = 0; i < NUM_ICONS; i++)
510 		g_clear_object (&icons[i]);
511 
512 	g_clear_pointer (&cache_thumbnails, g_hash_table_destroy);
513 	g_clear_object (&factory);
514 	g_thread_pool_free (thumbnail_pool, TRUE, FALSE);
515 	thumbnail_pool = NULL;
516 }
517 
518 void
totem_grilo_setup_icons(void)519 totem_grilo_setup_icons (void)
520 {
521 	icons[ICON_BOX] = load_named_icon ("folder-symbolic", VIDEO_ICON_SIZE, FILL_DEFAULT);
522 	icons[ICON_CHANNEL] = load_named_icon ("tv-symbolic", VIDEO_ICON_SIZE, FILL_DEFAULT);
523 	icons[ICON_VIDEO] = load_named_icon ("folder-videos-symbolic", VIDEO_ICON_SIZE, FILL_DEFAULT);
524 	icons[ICON_VIDEO_THUMBNAILING] = load_named_icon ("content-loading-symbolic", VIDEO_ICON_SIZE, FILL_TRANSPARENT);
525 	icons[ICON_OPTICAL] = load_named_icon ("media-optical-dvd-symbolic", VIDEO_ICON_SIZE, FILL_DEFAULT);
526 
527 	cache_thumbnails = g_hash_table_new_full (g_str_hash,
528 						  g_str_equal,
529 						  g_free,
530 						  g_object_unref);
531 
532 	factory = gnome_desktop_thumbnail_factory_new (GNOME_DESKTOP_THUMBNAIL_SIZE_LARGE);
533 	thumbnail_pool = g_thread_pool_new ((GFunc) thumbnail_media_async_thread, NULL, DEFAULT_MAX_THREADS, TRUE, NULL);
534 }
535 
536 void
totem_grilo_pause_icon_thumbnailing(void)537 totem_grilo_pause_icon_thumbnailing (void)
538 {
539 	g_return_if_fail (thumbnail_pool != NULL);
540 	g_thread_pool_set_max_threads (thumbnail_pool, 0, NULL);
541 }
542 
543 void
totem_grilo_resume_icon_thumbnailing(void)544 totem_grilo_resume_icon_thumbnailing (void)
545 {
546 	g_return_if_fail (thumbnail_pool != NULL);
547 	g_thread_pool_set_max_threads (thumbnail_pool, DEFAULT_MAX_THREADS, NULL);
548 }
549