Add missing space between switch and first parenthesis.
[geeqie.git] / src / metadata.c
1 /*
2  * Geeqie
3  * (C) 2004 John Ellis
4  * Copyright (C) 2008 - 2009 The Geeqie Team
5  *
6  * Author: John Ellis, Laurent Monin
7  *
8  * This software is released under the GNU General Public License (GNU GPL).
9  * Please read the included file COPYING for more information.
10  * This software comes with no warranty of any kind, use at your own risk!
11  */
12
13
14 #include "main.h"
15 #include "metadata.h"
16
17 #include "cache.h"
18 #include "exif.h"
19 #include "filedata.h"
20 #include "misc.h"
21 #include "secure_save.h"
22 #include "ui_fileops.h"
23 #include "ui_misc.h"
24 #include "utilops.h"
25 #include "filefilter.h"
26 #include "layout.h"
27
28 typedef enum {
29         MK_NONE,
30         MK_KEYWORDS,
31         MK_COMMENT
32 } MetadataKey;
33
34 static const gchar *group_keys[] = {KEYWORD_KEY, COMMENT_KEY, NULL}; /* tags that will be written to all files in a group */
35
36 static gboolean metadata_write_queue_idle_cb(gpointer data);
37 static gint metadata_legacy_write(FileData *fd);
38 static void metadata_legacy_delete(FileData *fd, const gchar *except);
39
40
41
42 /*
43  *-------------------------------------------------------------------
44  * write queue
45  *-------------------------------------------------------------------
46  */
47
48 static GList *metadata_write_queue = NULL;
49 static gint metadata_write_idle_id = -1;
50
51 static void metadata_write_queue_add(FileData *fd)
52 {
53         if (!g_list_find(metadata_write_queue, fd))
54                 {
55                 metadata_write_queue = g_list_prepend(metadata_write_queue, fd);
56                 file_data_ref(fd);
57                 
58                 layout_status_update_write_all();
59                 }
60
61         if (metadata_write_idle_id != -1) 
62                 {
63                 g_source_remove(metadata_write_idle_id);
64                 metadata_write_idle_id = -1;
65                 }
66         
67         if (options->metadata.confirm_after_timeout)
68                 {
69                 metadata_write_idle_id = g_timeout_add(options->metadata.confirm_timeout * 1000, metadata_write_queue_idle_cb, NULL);
70                 }
71 }
72
73
74 gboolean metadata_write_queue_remove(FileData *fd)
75 {
76         g_hash_table_destroy(fd->modified_xmp);
77         fd->modified_xmp = NULL;
78
79         metadata_write_queue = g_list_remove(metadata_write_queue, fd);
80         
81         file_data_increment_version(fd);
82         file_data_send_notification(fd, NOTIFY_TYPE_REREAD);
83
84         file_data_unref(fd);
85
86         layout_status_update_write_all();
87         return TRUE;
88 }
89
90 gboolean metadata_write_queue_remove_list(GList *list)
91 {
92         GList *work;
93         gboolean ret = TRUE;
94         
95         work = list;
96         while (work)
97                 {
98                 FileData *fd = work->data;
99                 work = work->next;
100                 ret = ret && metadata_write_queue_remove(fd);
101                 }
102         return ret;
103 }
104
105
106 gboolean metadata_write_queue_confirm(FileUtilDoneFunc done_func, gpointer done_data)
107 {
108         GList *work;
109         GList *to_approve = NULL;
110         
111         work = metadata_write_queue;
112         while (work)
113                 {
114                 FileData *fd = work->data;
115                 work = work->next;
116                 
117                 if (fd->change) continue; /* another operation in progress, skip this file for now */
118                 
119                 to_approve = g_list_prepend(to_approve, file_data_ref(fd));
120                 }
121
122         file_util_write_metadata(NULL, to_approve, NULL, done_func, done_data);
123         
124         filelist_free(to_approve);
125         
126         return (metadata_write_queue != NULL);
127 }
128
129 static gboolean metadata_write_queue_idle_cb(gpointer data)
130 {
131         metadata_write_queue_confirm(NULL, NULL);
132         metadata_write_idle_id = -1;
133         return FALSE;
134 }
135
136 gboolean metadata_write_perform(FileData *fd)
137 {
138         gboolean success;
139         ExifData *exif;
140         
141         g_assert(fd->change);
142         
143         if (fd->change->dest && 
144             strcmp(extension_from_path(fd->change->dest), GQ_CACHE_EXT_METADATA) == 0)
145                 {
146                 success = metadata_legacy_write(fd);
147                 if (success) metadata_legacy_delete(fd, fd->change->dest);
148                 return success;
149                 }
150
151         /* write via exiv2 */
152         /*  we can either use cached metadata which have fd->modified_xmp already applied 
153                                      or read metadata from file and apply fd->modified_xmp
154             metadata are read also if the file was modified meanwhile */
155         exif = exif_read_fd(fd); 
156         if (!exif) return FALSE;
157
158         success = (fd->change->dest) ? exif_write_sidecar(exif, fd->change->dest) : exif_write(exif); /* write modified metadata */
159         exif_free_fd(fd, exif);
160
161         if (fd->change->dest)
162                 /* this will create a FileData for the sidecar and link it to the main file 
163                    (we can't wait until the sidecar is discovered by directory scanning because
164                     exif_read_fd is called before that and it would read the main file only and 
165                     store the metadata in the cache)
166                     FIXME: this does not catch new sidecars created by independent external programs
167                 */
168                 file_data_unref(file_data_new_simple(fd->change->dest)); 
169                 
170         if (success) metadata_legacy_delete(fd, fd->change->dest);
171         return success;
172 }
173
174 gint metadata_queue_length(void)
175 {
176         return g_list_length(metadata_write_queue);
177 }
178
179 static gboolean metadata_check_key(const gchar *keys[], const gchar *key)
180 {
181         const gchar **k = keys;
182         
183         while (*k)
184                 {
185                 if (strcmp(key, *k) == 0) return TRUE;
186                 k++;
187                 }
188         return FALSE;
189 }
190
191 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
192 {
193         if (!fd->modified_xmp)
194                 {
195                 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)string_list_free);
196                 }
197         g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy((GList *)values));
198         if (fd->exif)
199                 {
200                 exif_update_metadata(fd->exif, key, values);
201                 }
202         metadata_write_queue_add(fd);
203         file_data_increment_version(fd);
204         file_data_send_notification(fd, NOTIFY_TYPE_INTERNAL);
205
206         if (options->metadata.sync_grouped_files && metadata_check_key(group_keys, key))
207                 {
208                 GList *work = fd->sidecar_files;
209                 
210                 while (work)
211                         {
212                         FileData *sfd = work->data;
213                         work = work->next;
214                         
215                         if (filter_file_class(sfd->extension, FORMAT_CLASS_META)) continue; 
216
217                         metadata_write_list(sfd, key, values);
218                         }
219                 }
220
221
222         return TRUE;
223 }
224         
225 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
226 {
227         GList *list = g_list_append(NULL, g_strdup(value));
228         gboolean ret = metadata_write_list(fd, key, list);
229         string_list_free(list);
230         return ret;
231 }
232
233
234 /*
235  *-------------------------------------------------------------------
236  * keyword / comment read/write
237  *-------------------------------------------------------------------
238  */
239
240 static gint metadata_file_write(gchar *path, GHashTable *modified_xmp)
241 {
242         SecureSaveInfo *ssi;
243         GList *keywords = g_hash_table_lookup(modified_xmp, KEYWORD_KEY);
244         GList *comment_l = g_hash_table_lookup(modified_xmp, COMMENT_KEY);
245         gchar *comment = comment_l ? comment_l->data : NULL;
246
247         ssi = secure_open(path);
248         if (!ssi) return FALSE;
249
250         secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
251
252         secure_fprintf(ssi, "[keywords]\n");
253         while (keywords && secsave_errno == SS_ERR_NONE)
254                 {
255                 const gchar *word = keywords->data;
256                 keywords = keywords->next;
257
258                 secure_fprintf(ssi, "%s\n", word);
259                 }
260         secure_fputc(ssi, '\n');
261
262         secure_fprintf(ssi, "[comment]\n");
263         secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
264
265         secure_fprintf(ssi, "#end\n");
266
267         return (secure_close(ssi) == 0);
268 }
269
270 static gint metadata_legacy_write(FileData *fd)
271 {
272         gint success = FALSE;
273
274         g_assert(fd->change && fd->change->dest);
275         gchar *metadata_pathl;
276
277         DEBUG_1("Saving comment: %s", fd->change->dest);
278
279         metadata_pathl = path_from_utf8(fd->change->dest);
280
281         success = metadata_file_write(metadata_pathl, fd->modified_xmp);
282
283         g_free(metadata_pathl);
284
285         return success;
286 }
287
288 static gint metadata_file_read(gchar *path, GList **keywords, gchar **comment)
289 {
290         FILE *f;
291         gchar s_buf[1024];
292         MetadataKey key = MK_NONE;
293         GList *list = NULL;
294         GString *comment_build = NULL;
295
296         f = fopen(path, "r");
297         if (!f) return FALSE;
298
299         while (fgets(s_buf, sizeof(s_buf), f))
300                 {
301                 gchar *ptr = s_buf;
302
303                 if (*ptr == '#') continue;
304                 if (*ptr == '[' && key != MK_COMMENT)
305                         {
306                         gchar *keystr = ++ptr;
307                         
308                         key = MK_NONE;
309                         while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
310                         
311                         if (*ptr == ']')
312                                 {
313                                 *ptr = '\0';
314                                 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
315                                         key = MK_KEYWORDS;
316                                 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
317                                         key = MK_COMMENT;
318                                 }
319                         continue;
320                         }
321                 
322                 switch (key)
323                         {
324                         case MK_NONE:
325                                 break;
326                         case MK_KEYWORDS:
327                                 {
328                                 while (*ptr != '\n' && *ptr != '\0') ptr++;
329                                 *ptr = '\0';
330                                 if (strlen(s_buf) > 0)
331                                         {
332                                         gchar *kw = utf8_validate_or_convert(s_buf);
333
334                                         list = g_list_prepend(list, kw);
335                                         }
336                                 }
337                                 break;
338                         case MK_COMMENT:
339                                 if (!comment_build) comment_build = g_string_new("");
340                                 g_string_append(comment_build, s_buf);
341                                 break;
342                         }
343                 }
344         
345         fclose(f);
346
347         if (keywords) 
348                 {
349                 *keywords = g_list_reverse(list);
350                 }
351         else
352                 {
353                 string_list_free(list);
354                 }
355                 
356         if (comment_build)
357                 {
358                 if (comment)
359                         {
360                         gint len;
361                         gchar *ptr = comment_build->str;
362
363                         /* strip leading and trailing newlines */
364                         while (*ptr == '\n') ptr++;
365                         len = strlen(ptr);
366                         while (len > 0 && ptr[len - 1] == '\n') len--;
367                         if (ptr[len] == '\n') len++; /* keep the last one */
368                         if (len > 0)
369                                 {
370                                 gchar *text = g_strndup(ptr, len);
371
372                                 *comment = utf8_validate_or_convert(text);
373                                 g_free(text);
374                                 }
375                         }
376                 g_string_free(comment_build, TRUE);
377                 }
378
379         return TRUE;
380 }
381
382 static void metadata_legacy_delete(FileData *fd, const gchar *except)
383 {
384         gchar *metadata_path;
385         gchar *metadata_pathl;
386         if (!fd) return;
387
388         metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
389         if (metadata_path && (!except || strcmp(metadata_path, except) != 0)) 
390                 {
391                 metadata_pathl = path_from_utf8(metadata_path);
392                 unlink(metadata_pathl);
393                 g_free(metadata_pathl);
394                 g_free(metadata_path);
395                 }
396         metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
397         if (metadata_path && (!except || strcmp(metadata_path, except) != 0)) 
398                 {
399                 metadata_pathl = path_from_utf8(metadata_path);
400                 unlink(metadata_pathl);
401                 g_free(metadata_pathl);
402                 g_free(metadata_path);
403                 }
404 }
405
406 static gint metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
407 {
408         gchar *metadata_path;
409         gchar *metadata_pathl;
410         gint success = FALSE;
411         if (!fd) return FALSE;
412
413         metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
414         if (!metadata_path) return FALSE;
415
416         metadata_pathl = path_from_utf8(metadata_path);
417
418         success = metadata_file_read(metadata_pathl, keywords, comment);
419
420         g_free(metadata_pathl);
421         g_free(metadata_path);
422
423         return success;
424 }
425
426 static GList *remove_duplicate_strings_from_list(GList *list)
427 {
428         GList *work = list;
429         GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
430         GList *newlist = NULL;
431
432         while (work)
433                 {
434                 gchar *key = work->data;
435
436                 if (g_hash_table_lookup(hashtable, key) == NULL)
437                         {
438                         g_hash_table_insert(hashtable, (gpointer) key, GINT_TO_POINTER(1));
439                         newlist = g_list_prepend(newlist, key);
440                         }
441                 work = work->next;
442                 }
443
444         g_hash_table_destroy(hashtable);
445         g_list_free(list);
446
447         return g_list_reverse(newlist);
448 }
449
450 GList *metadata_read_list(FileData *fd, const gchar *key, MetadataFormat format)
451 {
452         ExifData *exif;
453         GList *list = NULL;
454         if (!fd) return NULL;
455
456         /* unwritten data overide everything */
457         if (fd->modified_xmp && format == METADATA_PLAIN)
458                 {
459                 list = g_hash_table_lookup(fd->modified_xmp, key);
460                 if (list) return string_list_copy(list);
461                 }
462
463         /* 
464             Legacy metadata file is the primary source if it exists.
465             Merging the lists does not make much sense, because the existence of
466             legacy metadata file indicates that the other metadata sources are not
467             writable and thus it would not be possible to delete the keywords
468             that comes from the image file.
469         */
470         if (strcmp(key, KEYWORD_KEY) == 0)
471                 {
472                 if (metadata_legacy_read(fd, &list, NULL)) return list;
473                 }
474
475         if (strcmp(key, COMMENT_KEY) == 0)
476                 {
477                 gchar *comment = NULL;
478                 if (metadata_legacy_read(fd, NULL, &comment)) return g_list_append(NULL, comment);
479                 }
480         
481         exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
482         if (!exif) return NULL;
483         list = exif_get_metadata(exif, key, format);
484         exif_free_fd(fd, exif);
485         return list;
486 }
487
488 gchar *metadata_read_string(FileData *fd, const gchar *key, MetadataFormat format)
489 {
490         GList *string_list = metadata_read_list(fd, key, format);
491         if (string_list)
492                 {
493                 gchar *str = string_list->data;
494                 string_list->data = NULL;
495                 string_list_free(string_list);
496                 return str;
497                 }
498         return NULL;
499 }
500
501 guint64 metadata_read_int(FileData *fd, const gchar *key, guint64 fallback)
502 {
503         guint64 ret;
504         gchar *endptr;
505         gchar *string = metadata_read_string(fd, key, METADATA_PLAIN);
506         if (!string) return fallback;
507         
508         ret = g_ascii_strtoull(string, &endptr, 10);
509         if (string == endptr) ret = fallback;
510         g_free(string);
511         return ret;
512 }
513         
514 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
515 {
516         gchar *str = metadata_read_string(fd, key, METADATA_PLAIN);
517         
518         if (!str) 
519                 {
520                 return metadata_write_string(fd, key, value);
521                 }
522         else
523                 {
524                 gchar *new_string = g_strconcat(str, value, NULL);
525                 gboolean ret = metadata_write_string(fd, key, new_string);
526                 g_free(str);
527                 g_free(new_string);
528                 return ret;
529                 }
530 }
531
532 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
533 {
534         GList *list = metadata_read_list(fd, key, METADATA_PLAIN);
535         
536         if (!list) 
537                 {
538                 return metadata_write_list(fd, key, values);
539                 }
540         else
541                 {
542                 gboolean ret;
543                 list = g_list_concat(list, string_list_copy(values));
544                 list = remove_duplicate_strings_from_list(list);
545                 
546                 ret = metadata_write_list(fd, key, list);
547                 string_list_free(list);
548                 return ret;
549                 }
550 }
551
552 gchar *find_string_in_list_utf8nocase(GList *list, const gchar *string)
553 {
554         gchar *string_casefold = g_utf8_casefold(string, -1);
555
556         while (list)
557                 {
558                 gchar *haystack = list->data;
559                 
560                 if (haystack)
561                         {
562                         gboolean equal;
563                         gchar *haystack_casefold = g_utf8_casefold(haystack, -1);
564
565                         equal = (strcmp(haystack_casefold, string_casefold) == 0);
566                         g_free(haystack_casefold);
567
568                         if (equal)
569                                 {
570                                 g_free(string_casefold);
571                                 return haystack;
572                                 }
573                         }
574         
575                 list = list->next;
576                 }
577         
578         g_free(string_casefold);
579         return NULL;
580 }
581
582
583 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
584
585 GList *string_to_keywords_list(const gchar *text)
586 {
587         GList *list = NULL;
588         const gchar *ptr = text;
589
590         while (*ptr != '\0')
591                 {
592                 const gchar *begin;
593                 gint l = 0;
594
595                 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
596                 begin = ptr;
597                 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
598                         {
599                         ptr++;
600                         l++;
601                         }
602
603                 /* trim starting and ending whitespaces */
604                 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
605                 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
606
607                 if (l > 0)
608                         {
609                         gchar *keyword = g_strndup(begin, l);
610
611                         /* only add if not already in the list */
612                         if (!find_string_in_list_utf8nocase(list, keyword))
613                                 list = g_list_append(list, keyword);
614                         else
615                                 g_free(keyword);
616                         }
617                 }
618
619         return list;
620 }
621
622 /*
623  * keywords to marks
624  */
625  
626
627 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
628 {
629         GList *keywords;
630         gboolean found = FALSE;
631         keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
632         if (keywords)
633                 {
634                 GList *work = keywords;
635
636                 while (work)
637                         {
638                         gchar *kw = work->data;
639                         work = work->next;
640                         
641                         if (strcmp(kw, data) == 0)
642                                 {
643                                 found = TRUE;
644                                 break;
645                                 }
646                         }
647                 string_list_free(keywords);
648                 }
649         return found;
650 }
651
652 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
653 {
654         GList *keywords = NULL;
655         gboolean found = FALSE;
656         gboolean changed = FALSE;
657         GList *work;
658         keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN);
659
660         work = keywords;
661
662         while (work)
663                 {
664                 gchar *kw = work->data;
665                 
666                 if (strcmp(kw, data) == 0)
667                         {
668                         found = TRUE;
669                         if (!value) 
670                                 {
671                                 changed = TRUE;
672                                 keywords = g_list_delete_link(keywords, work);
673                                 g_free(kw);
674                                 }
675                         break;
676                         }
677                 work = work->next;
678                 }
679         if (value && !found) 
680                 {
681                 changed = TRUE;
682                 keywords = g_list_append(keywords, g_strdup(data));
683                 }
684         
685         if (changed) metadata_write_list(fd, KEYWORD_KEY, keywords);
686
687         string_list_free(keywords);
688         return TRUE;
689 }
690
691
692 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */