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