1 /*
2  * frogr-util.c -- Misc tools.
3  *
4  * Copyright (C) 2010-2012 Mario Sanchez Prada
5  * Authors: Mario Sanchez Prada <msanchez@gnome.org>
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of version 3 of the GNU General Public
9  * License as published by the Free Software Foundation.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, see <http://www.gnu.org/licenses/>
18  *
19  * Parts of this file based on code from gst-plugins-base, licensed as
20  * GPL version 2 or later (Copyright (C) <2007> Wim Taymans <wim.taymans@gmail.com>)
21  */
22 
23 #include "frogr-picture.h"
24 #include "frogr-util.h"
25 #include "frogr-global-defs.h"
26 
27 #include <config.h>
28 #include <glib/gi18n.h>
29 #include <gtk/gtk.h>
30 #include <gst/gst.h>
31 #include <libexif/exif-byte-order.h>
32 #include <libexif/exif-data.h>
33 #include <libexif/exif-entry.h>
34 #include <libexif/exif-format.h>
35 #include <libexif/exif-loader.h>
36 #include <libexif/exif-tag.h>
37 
38 #define CAPS "video/x-raw-rgb,width=160,pixel-aspect-ratio=1/1,bpp=(int)24,depth=(int)24,endianness=(int)4321,red_mask=(int)0xff0000, green_mask=(int)0x00ff00, blue_mask=(int)0x0000ff"
39 
40 static gboolean
_spawn_command(const gchar * cmd)41 _spawn_command (const gchar* cmd)
42 {
43   GError *error = NULL;
44 
45   if (!g_spawn_command_line_async (cmd, &error))
46     {
47       if (error)
48         {
49           DEBUG ("Error spawning command '%s': %s", cmd, error->message);
50           g_error_free (error);
51         }
52 
53       return FALSE;
54     }
55   return TRUE;
56 }
57 
58 const gchar *
_get_data_dir(void)59 _get_data_dir (void)
60 {
61 #ifdef PLATFORM_MAC
62   /* For MacOSX, we return the value of the environment value set by
63      the wrapper script running the application */
64   static gchar *xdg_data_dir = NULL;
65   if (!xdg_data_dir)
66     xdg_data_dir = g_strdup (g_getenv("XDG_DATA_DIRS"));
67   return (const gchar *) xdg_data_dir;
68 #else
69   /* For GNOME, we just return DATA_DIR */
70   return DATA_DIR;
71 #endif
72 }
73 
74 const gchar *
frogr_util_get_app_data_dir(void)75 frogr_util_get_app_data_dir (void)
76 {
77   static gchar *app_data_dir = NULL;
78   if (!app_data_dir)
79     app_data_dir = g_strdup_printf ("%s/frogr", _get_data_dir ());
80 
81   return (const gchar *) app_data_dir;
82 }
83 
84 const gchar *
frogr_util_get_icons_dir(void)85 frogr_util_get_icons_dir (void)
86 {
87   static gchar *icons_dir = NULL;
88   if (!icons_dir)
89     icons_dir = g_strdup_printf ("%s/icons", _get_data_dir ());
90 
91   return (const gchar *) icons_dir;
92 }
93 
94 const gchar *
frogr_util_get_locale_dir(void)95 frogr_util_get_locale_dir (void)
96 {
97   static const gchar *locale_dir = NULL;
98   if (!locale_dir)
99     {
100 #ifndef PLATFORM_MAC
101       /* If not in MacOSX, we trust the defined variable better */
102       locale_dir = g_strdup (FROGR_LOCALE_DIR);
103 #endif
104 
105       /* Fallback for MacOSX and cases where FROGR_LOCALE_DIR was not
106 	 defined yet because of any reason */
107       if (!locale_dir)
108 	locale_dir = g_strdup_printf ("%s/locale", _get_data_dir ());
109     }
110 
111   return (const gchar *) locale_dir;
112 }
113 
114 gchar *
_get_uris_string_from_list(GList * uris_list)115 _get_uris_string_from_list (GList *uris_list)
116 {
117   GList *current_uri = NULL;
118   gchar **uris_array = NULL;
119   gchar *uris_str = NULL;
120   gint n_uris = 0;
121   gint i = 0;
122 
123   n_uris = g_list_length (uris_list);
124   if (n_uris == 0)
125     return NULL;
126 
127   uris_array = g_new0 (gchar*, n_uris + 1);
128   for (current_uri = uris_list; current_uri; current_uri = g_list_next (current_uri))
129     uris_array[i++] = (gchar *) (current_uri->data);
130 
131   uris_str = g_strjoinv (" ", uris_array);
132   g_free (uris_array);
133 
134   return uris_str;
135 }
136 
137 static void
_open_uris_with_app_info(GList * uris_list,GAppInfo * app_info)138 _open_uris_with_app_info (GList *uris_list, GAppInfo *app_info)
139 {
140   GError *error = NULL;
141 
142   /* Early return */
143   if (!uris_list)
144     return;
145 
146   if (!app_info || !g_app_info_launch_uris (app_info, uris_list, NULL, &error))
147     {
148       /* The default app didn't succeed, so try 'gnome-open' / 'open' */
149       gchar *command = NULL;
150       gchar *uris = NULL;
151 
152       uris = _get_uris_string_from_list (uris_list);
153 
154 #ifdef PLATFORM_MAC
155       /* In MacOSX use 'open' instead of 'gnome-open' */
156       command = g_strdup_printf ("open %s", uris);
157 #else
158       command = g_strdup_printf ("gnome-open %s", uris);
159 #endif
160       _spawn_command (command);
161 
162       if (error)
163         {
164           DEBUG ("Error opening URI(s) %s: %s", uris, error->message);
165           g_error_free (error);
166         }
167 
168       g_free (command);
169       g_free (uris);
170     }
171 
172   g_list_foreach (uris_list, (GFunc) g_free, NULL);
173   g_list_free (uris_list);
174 }
175 
176 void
frogr_util_open_uri(const gchar * uri)177 frogr_util_open_uri (const gchar *uri)
178 {
179   GAppInfo *app_info = NULL;
180   GList *uris_list = NULL;
181 
182   /* Early return */
183   if (!uri)
184     return;
185 
186 #ifndef PLATFORM_MAC
187   /* Supported network URIs */
188   if (g_str_has_prefix (uri, "http:") || g_str_has_prefix (uri, "https:"))
189     app_info = g_app_info_get_default_for_uri_scheme ("http");
190 
191   /* Supported help URIs */
192   if (g_str_has_prefix (uri, "ghelp:"))
193     app_info = g_app_info_get_default_for_uri_scheme ("ghelp");
194 #endif
195 
196   uris_list = g_list_append (uris_list, g_strdup (uri));
197   _open_uris_with_app_info (uris_list, app_info);
198 }
199 
200 void
frogr_util_open_pictures_in_viewer(GSList * pictures)201 frogr_util_open_pictures_in_viewer (GSList *pictures)
202 {
203   GAppInfo *app_info = NULL;
204   GList *uris_list = NULL;
205   GSList *current_pic = NULL;
206   FrogrPicture *picture = NULL;
207 
208   /* Early return */
209   if (!pictures)
210     return;
211 
212   for (current_pic = pictures; current_pic; current_pic = g_slist_next (current_pic))
213     {
214       picture = FROGR_PICTURE (current_pic->data);
215       uris_list = g_list_append (uris_list, g_strdup (frogr_picture_get_fileuri (picture)));
216     }
217 
218   /* We currently choose the application based in the mime type of the
219      first picture. This is very basic, but probably good enough for now */
220   picture = FROGR_PICTURE (pictures->data);
221   if (frogr_picture_is_video (picture))
222     app_info = g_app_info_get_default_for_type ("video/mpeg", TRUE);
223   else
224     app_info = g_app_info_get_default_for_type ("image/jpg", TRUE);
225 
226   /* uris_list will be freed inside of the function */
227   _open_uris_with_app_info (uris_list, app_info);
228 }
229 
230 static void
_show_message_dialog(GtkWindow * parent,const gchar * message,GtkMessageType type)231 _show_message_dialog (GtkWindow *parent, const gchar *message, GtkMessageType type)
232 {
233   /* Show alert */
234   GtkWidget *dialog =
235     gtk_message_dialog_new (parent,
236                             GTK_DIALOG_MODAL,
237                             type,
238                             GTK_BUTTONS_CLOSE,
239                             "%s", message);
240   gtk_window_set_title (GTK_WINDOW (dialog), APP_SHORTNAME);
241 
242   g_signal_connect (G_OBJECT (dialog), "response",
243                     G_CALLBACK (gtk_widget_destroy), dialog);
244 
245   gtk_widget_show_all (dialog);
246 }
247 
248 void
frogr_util_show_info_dialog(GtkWindow * parent,const gchar * message)249 frogr_util_show_info_dialog (GtkWindow *parent, const gchar *message)
250 {
251   _show_message_dialog (parent, message, GTK_MESSAGE_INFO);
252 }
253 
254 void
frogr_util_show_warning_dialog(GtkWindow * parent,const gchar * message)255 frogr_util_show_warning_dialog (GtkWindow *parent, const gchar *message)
256 {
257   _show_message_dialog (parent, message, GTK_MESSAGE_WARNING);
258 }
259 
260 void
frogr_util_show_error_dialog(GtkWindow * parent,const gchar * message)261 frogr_util_show_error_dialog (GtkWindow *parent, const gchar *message)
262 {
263   _show_message_dialog (parent, message, GTK_MESSAGE_ERROR);
264 }
265 
266 static GdkPixbuf *
_get_corrected_pixbuf(GdkPixbuf * pixbuf,gint max_width,gint max_height)267 _get_corrected_pixbuf (GdkPixbuf *pixbuf, gint max_width, gint max_height)
268 {
269   GdkPixbuf *scaled_pixbuf = NULL;
270   GdkPixbuf *rotated_pixbuf;
271   const gchar *orientation;
272   gint width;
273   gint height;
274 
275   g_return_val_if_fail (max_width > 0, NULL);
276   g_return_val_if_fail (max_height > 0, NULL);
277 
278   /* Look for the right side to reduce */
279   width = gdk_pixbuf_get_width (pixbuf);
280   height = gdk_pixbuf_get_height (pixbuf);
281 
282   DEBUG ("Original size: %dx%d\n", width, height);
283 
284   if (width > max_width)
285     {
286       height = (float)height * max_width / width;
287       width = max_width;
288     }
289 
290   if (height > max_height)
291     {
292       width = (float)width * max_height / height;
293       height = max_height;
294     }
295 
296   DEBUG ("Scaled size: %dx%d\n", width, height);
297 
298   /* Scale the pixbuf to its best size */
299   scaled_pixbuf = gdk_pixbuf_scale_simple (pixbuf, width, height,
300                                            GDK_INTERP_BILINEAR);
301 
302   /* Correct orientation if needed */
303   orientation = gdk_pixbuf_get_option (pixbuf, "orientation");
304 
305   /* No orientation defined or 0 degrees rotation: we're done */
306   if (!orientation || !g_strcmp0 (orientation, "1"))
307     return scaled_pixbuf;
308 
309   DEBUG ("File orientation for file: %s", orientation);
310   rotated_pixbuf = NULL;
311 
312   /* Rotated 90 degrees */
313   if (!g_strcmp0 (orientation, "8"))
314     rotated_pixbuf = gdk_pixbuf_rotate_simple (scaled_pixbuf,
315                                                GDK_PIXBUF_ROTATE_COUNTERCLOCKWISE);
316   /* Rotated 180 degrees */
317   if (!g_strcmp0 (orientation, "3"))
318     rotated_pixbuf = gdk_pixbuf_rotate_simple (scaled_pixbuf,
319                                                GDK_PIXBUF_ROTATE_UPSIDEDOWN);
320   /* Rotated 270 degrees */
321   if (!g_strcmp0 (orientation, "6"))
322     rotated_pixbuf = gdk_pixbuf_rotate_simple (scaled_pixbuf,
323                                                GDK_PIXBUF_ROTATE_CLOCKWISE);
324   if (rotated_pixbuf)
325     {
326       g_object_unref (scaled_pixbuf);
327       return rotated_pixbuf;
328     }
329 
330   /* No rotation was applied, return the scaled pixbuf */
331   return scaled_pixbuf;
332 }
333 
334 static GdkPixbuf *
_get_pixbuf_from_image_contents(const guchar * contents,gsize length,GError ** out_error)335 _get_pixbuf_from_image_contents (const guchar *contents, gsize length, GError **out_error)
336 {
337   GdkPixbufLoader *pixbuf_loader = NULL;
338   GdkPixbuf *pixbuf = NULL;
339   GError *error = NULL;
340 
341   pixbuf_loader = gdk_pixbuf_loader_new ();
342   if (gdk_pixbuf_loader_write (pixbuf_loader,
343                                (const guchar *)contents,
344                                length,
345                                &error))
346     {
347       gdk_pixbuf_loader_close (pixbuf_loader, NULL);
348       pixbuf = gdk_pixbuf_loader_get_pixbuf (pixbuf_loader);
349     }
350 
351   if (error)
352     {
353       DEBUG ("Error loading pixbuf: %s", error->message);
354       g_propagate_error (out_error, error);
355     }
356 
357   /* Keep the pixbuf before destroying the loader */
358   if (pixbuf)
359     g_object_ref (pixbuf);
360   g_object_unref (pixbuf_loader);
361 
362   return pixbuf;
363 }
364 
365 /* The following function is based in GStreamer's snapshot example,
366    from gst-plugins-base, licensed as GPL version 2 or later
367    (Copyright (C) <2007> Wim Taymans <wim.taymans@gmail.com>) */
368 static GdkPixbuf *
_get_pixbuf_from_video_file(GFile * file,GError ** out_error)369 _get_pixbuf_from_video_file (GFile *file, GError **out_error)
370 {
371   GdkPixbuf *pixbuf = NULL;
372   GstElement *pipeline, *sink;
373   GstBuffer *buffer;
374   GstFormat format;
375   GstStateChangeReturn ret;
376   gchar *file_uri;
377   gchar *descr;
378   gint width, height;
379   gint64 duration, position;
380   GError *error = NULL;
381   gboolean res;
382 
383   /* create a new pipeline */
384   file_uri = g_file_get_uri (file);
385   descr = g_strdup_printf ("uridecodebin uri=%s ! ffmpegcolorspace ! videoscale ! "
386                            " appsink name=sink caps=\"" CAPS "\"", file_uri);
387   g_free (file_uri);
388 
389   pipeline = gst_parse_launch (descr, &error);
390   g_free (descr);
391 
392   if (error != NULL) {
393     DEBUG ("Could not construct pipeline: %s\n", error->message);
394     g_propagate_error (out_error, error);
395     return NULL;
396   }
397 
398   /* get sink */
399   sink = gst_bin_get_by_name (GST_BIN (pipeline), "sink");
400 
401   /* set to PAUSED to make the first frame arrive in the sink */
402   ret = gst_element_set_state (pipeline, GST_STATE_PAUSED);
403   switch (ret) {
404     case GST_STATE_CHANGE_FAILURE:
405       DEBUG ("failed to play the file\n");
406       return NULL;
407     case GST_STATE_CHANGE_NO_PREROLL:
408       /* for live sources, we need to set the pipeline to PLAYING before we can
409        * receive a buffer. We don't do that yet */
410       DEBUG ("live sources not supported yet\n");
411       return NULL;
412     default:
413       break;
414   }
415 
416   /* This can block for up to 5 seconds. If your machine is really overloaded,
417    * it might time out before the pipeline prerolled and we generate an error. A
418    * better way is to run a mainloop and catch errors there. */
419   ret = gst_element_get_state (pipeline, NULL, NULL, 5 * GST_SECOND);
420   if (ret == GST_STATE_CHANGE_FAILURE) {
421     DEBUG ("failed to play the file\n");
422     return NULL;
423   }
424 
425   /* get the duration */
426   format = GST_FORMAT_TIME;
427   gst_element_query_duration (pipeline, &format, &duration);
428 
429   if (duration != -1)
430     /* we have a duration, seek to 50% */
431     position = duration * 0.5;
432   else
433     /* no duration, seek to 1 second, this could EOS */
434     position = 1 * GST_SECOND;
435 
436   /* seek to the a position in the file. Most files have a black first frame so
437    * by seeking to somewhere else we have a bigger chance of getting something
438    * more interesting. */
439   gst_element_seek_simple (pipeline, GST_FORMAT_TIME,
440       GST_SEEK_FLAG_KEY_UNIT | GST_SEEK_FLAG_FLUSH, position);
441 
442   /* get the preroll buffer from appsink, this block untils appsink really prerolls */
443   g_signal_emit_by_name (sink, "pull-preroll", &buffer, NULL);
444 
445   /* if we have a buffer now, convert it to a pixbuf. It's possible that we
446    * don't have a buffer because we went EOS right away or had an error. */
447   if (buffer) {
448     GstCaps *caps;
449     GstStructure *s;
450 
451     /* get the snapshot buffer format now. We set the caps on the appsink so
452      * that it can only be an rgb buffer. The only thing we have not specified
453      * on the caps is the height, which is dependant on the pixel-aspect-ratio
454      * of the source material */
455     caps = GST_BUFFER_CAPS (buffer);
456     if (!caps) {
457       DEBUG ("could not get snapshot format\n");
458       return NULL;
459     }
460     s = gst_caps_get_structure (caps, 0);
461 
462     /* we need to get the final caps on the buffer to get the size */
463     res = gst_structure_get_int (s, "width", &width);
464     res |= gst_structure_get_int (s, "height", &height);
465     if (!res) {
466       DEBUG ("could not get snapshot dimension\n");
467       return NULL;
468     }
469 
470     /* create pixmap from buffer and save, gstreamer video buffers have a stride
471      * that is rounded up to the nearest multiple of 4 */
472     pixbuf = gdk_pixbuf_new_from_data (GST_BUFFER_DATA (buffer),
473         GDK_COLORSPACE_RGB, FALSE, 8, width, height,
474         GST_ROUND_UP_4 (width * 3), NULL, NULL);
475 
476   } else {
477     DEBUG ("could not make snapshot\n");
478   }
479 
480   /* cleanup and exit */
481   gst_element_set_state (pipeline, GST_STATE_NULL);
482   gst_object_unref (pipeline);
483 
484   return pixbuf;
485 }
486 
487 GdkPixbuf *
frogr_util_get_pixbuf_for_video_file(GFile * file,gint max_width,gint max_height,GError ** error)488 frogr_util_get_pixbuf_for_video_file (GFile *file, gint max_width, gint max_height, GError **error)
489 {
490   GdkPixbuf *pixbuf = NULL;
491 
492   pixbuf = _get_pixbuf_from_video_file (file, error);
493   if (pixbuf)
494     {
495       GdkPixbuf *c_pixbuf = NULL;
496       c_pixbuf = _get_corrected_pixbuf (pixbuf, max_width, max_height);
497       g_object_unref (pixbuf);
498       pixbuf = c_pixbuf;
499     }
500 
501   return pixbuf;
502 }
503 
504 GdkPixbuf *
frogr_util_get_pixbuf_from_image_contents(const guchar * contents,gsize length,gint max_width,gint max_height,GError ** error)505 frogr_util_get_pixbuf_from_image_contents (const guchar *contents, gsize length, gint max_width, gint max_height, GError **error)
506 {
507   GdkPixbuf *pixbuf = NULL;
508 
509   pixbuf = _get_pixbuf_from_image_contents (contents, length, error);
510   if (pixbuf)
511     {
512       GdkPixbuf *c_pixbuf = NULL;
513       c_pixbuf = _get_corrected_pixbuf (pixbuf, max_width, max_height);
514       g_object_unref (pixbuf);
515       pixbuf = c_pixbuf;
516     }
517 
518   return pixbuf;
519 }
520 
521 gchar *
frogr_util_get_datasize_string(gulong datasize)522 frogr_util_get_datasize_string (gulong datasize)
523 {
524   gchar *result = NULL;
525 
526   if (datasize != G_MAXULONG)
527     {
528       gfloat datasize_float = G_MAXFLOAT;
529       gchar *unit_str = NULL;
530       int n_divisions = 0;
531 
532       datasize_float = datasize;
533       while (datasize_float > 1000.0 && n_divisions < 3)
534         {
535           datasize_float /= 1024;
536           n_divisions++;
537         }
538 
539       switch (n_divisions)
540         {
541         case 0:
542           unit_str = g_strdup ("KB");
543           break;
544         case 1:
545           unit_str = g_strdup ("MB");
546           break;
547         case 2:
548           unit_str = g_strdup ("GB");
549           break;
550         default:
551           unit_str = NULL;;
552         }
553 
554       if (unit_str)
555         {
556           result = g_strdup_printf ("%.1f %s", datasize_float, unit_str);
557           g_free (unit_str);
558         }
559     }
560 
561   return result;
562 }
563 
564 const gchar * const *
frogr_util_get_supported_mimetypes(void)565 frogr_util_get_supported_mimetypes (void)
566 {
567   static const gchar *supported_mimetypes[] = {
568     "image/jpg",
569     "image/jpeg",
570     "image/png",
571     "image/bmp",
572     "image/gif",
573     "video/mpeg",
574     "video/mp4",
575     "video/quicktime",
576     "video/x-msvideo",
577     "video/ogg",
578     "video/x-ms-wmv",
579     "video/3gpp",
580     "video/m2ts",
581     "video/avchd-stream",
582     "video/mp2t",
583     "video/vnd.dlna.mpeg-tts",
584     "application/ogg",
585     NULL
586   };
587 
588   return supported_mimetypes;
589 }
590