improved metadata preferences
[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 #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 (success) metadata_legacy_delete(fd, fd->change->dest);
162         return success;
163 }
164
165 gint metadata_queue_length(void)
166 {
167         return g_list_length(metadata_write_queue);
168 }
169
170 static gboolean metadata_check_key(const gchar *keys[], const gchar *key)
171 {
172         const gchar **k = keys;
173         
174         while (*k)
175                 {
176                 if (strcmp(key, *k) == 0) return TRUE;
177                 k++;
178                 }
179         return FALSE;
180 }
181
182 gboolean metadata_write_list(FileData *fd, const gchar *key, const GList *values)
183 {
184         if (!fd->modified_xmp)
185                 {
186                 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)string_list_free);
187                 }
188         g_hash_table_insert(fd->modified_xmp, g_strdup(key), string_list_copy((GList *)values));
189         if (fd->exif)
190                 {
191                 exif_update_metadata(fd->exif, key, values);
192                 }
193         metadata_write_queue_add(fd);
194         file_data_increment_version(fd);
195         file_data_send_notification(fd, NOTIFY_TYPE_INTERNAL);
196
197         if (options->metadata.sync_grouped_files && metadata_check_key(group_keys, key))
198                 {
199                 GList *work = fd->sidecar_files;
200                 
201                 while (work)
202                         {
203                         FileData *sfd = work->data;
204                         work = work->next;
205                         
206                         if (filter_file_class(sfd->extension, FORMAT_CLASS_META)) continue; 
207
208                         metadata_write_list(sfd, key, values);
209                         }
210                 }
211
212
213         return TRUE;
214 }
215         
216 gboolean metadata_write_string(FileData *fd, const gchar *key, const char *value)
217 {
218         GList *list = g_list_append(NULL, g_strdup(value));
219         gboolean ret = metadata_write_list(fd, key, list);
220         string_list_free(list);
221         return ret;
222 }
223
224
225 /*
226  *-------------------------------------------------------------------
227  * keyword / comment read/write
228  *-------------------------------------------------------------------
229  */
230
231 static gint metadata_file_write(gchar *path, GHashTable *modified_xmp)
232 {
233         SecureSaveInfo *ssi;
234         GList *keywords = g_hash_table_lookup(modified_xmp, KEYWORD_KEY);
235         GList *comment_l = g_hash_table_lookup(modified_xmp, COMMENT_KEY);
236         gchar *comment = comment_l ? comment_l->data : NULL;
237
238         ssi = secure_open(path);
239         if (!ssi) return FALSE;
240
241         secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
242
243         secure_fprintf(ssi, "[keywords]\n");
244         while (keywords && secsave_errno == SS_ERR_NONE)
245                 {
246                 const gchar *word = keywords->data;
247                 keywords = keywords->next;
248
249                 secure_fprintf(ssi, "%s\n", word);
250                 }
251         secure_fputc(ssi, '\n');
252
253         secure_fprintf(ssi, "[comment]\n");
254         secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
255
256         secure_fprintf(ssi, "#end\n");
257
258         return (secure_close(ssi) == 0);
259 }
260
261 static gint metadata_legacy_write(FileData *fd)
262 {
263         gint success = FALSE;
264
265         g_assert(fd->change && fd->change->dest);
266         gchar *metadata_pathl;
267
268         DEBUG_1("Saving comment: %s", fd->change->dest);
269
270         metadata_pathl = path_from_utf8(fd->change->dest);
271
272         success = metadata_file_write(metadata_pathl, fd->modified_xmp);
273
274         g_free(metadata_pathl);
275
276         return success;
277 }
278
279 static gint metadata_file_read(gchar *path, GList **keywords, gchar **comment)
280 {
281         FILE *f;
282         gchar s_buf[1024];
283         MetadataKey key = MK_NONE;
284         GList *list = NULL;
285         GString *comment_build = NULL;
286
287         f = fopen(path, "r");
288         if (!f) return FALSE;
289
290         while (fgets(s_buf, sizeof(s_buf), f))
291                 {
292                 gchar *ptr = s_buf;
293
294                 if (*ptr == '#') continue;
295                 if (*ptr == '[' && key != MK_COMMENT)
296                         {
297                         gchar *keystr = ++ptr;
298                         
299                         key = MK_NONE;
300                         while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
301                         
302                         if (*ptr == ']')
303                                 {
304                                 *ptr = '\0';
305                                 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
306                                         key = MK_KEYWORDS;
307                                 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
308                                         key = MK_COMMENT;
309                                 }
310                         continue;
311                         }
312                 
313                 switch(key)
314                         {
315                         case MK_NONE:
316                                 break;
317                         case MK_KEYWORDS:
318                                 {
319                                 while (*ptr != '\n' && *ptr != '\0') ptr++;
320                                 *ptr = '\0';
321                                 if (strlen(s_buf) > 0)
322                                         {
323                                         gchar *kw = utf8_validate_or_convert(s_buf);
324
325                                         list = g_list_prepend(list, kw);
326                                         }
327                                 }
328                                 break;
329                         case MK_COMMENT:
330                                 if (!comment_build) comment_build = g_string_new("");
331                                 g_string_append(comment_build, s_buf);
332                                 break;
333                         }
334                 }
335         
336         fclose(f);
337
338         if (keywords) 
339                 {
340                 *keywords = g_list_reverse(list);
341                 }
342         else
343                 {
344                 string_list_free(list);
345                 }
346                 
347         if (comment_build)
348                 {
349                 if (comment)
350                         {
351                         gint len;
352                         gchar *ptr = comment_build->str;
353
354                         /* strip leading and trailing newlines */
355                         while (*ptr == '\n') ptr++;
356                         len = strlen(ptr);
357                         while (len > 0 && ptr[len - 1] == '\n') len--;
358                         if (ptr[len] == '\n') len++; /* keep the last one */
359                         if (len > 0)
360                                 {
361                                 gchar *text = g_strndup(ptr, len);
362
363                                 *comment = utf8_validate_or_convert(text);
364                                 g_free(text);
365                                 }
366                         }
367                 g_string_free(comment_build, TRUE);
368                 }
369
370         return TRUE;
371 }
372
373 static void metadata_legacy_delete(FileData *fd, const gchar *except)
374 {
375         gchar *metadata_path;
376         gchar *metadata_pathl;
377         if (!fd) return;
378
379         metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
380         if (metadata_path && (!except || strcmp(metadata_path, except) != 0)) 
381                 {
382                 metadata_pathl = path_from_utf8(metadata_path);
383                 unlink(metadata_pathl);
384                 g_free(metadata_pathl);
385                 g_free(metadata_path);
386                 }
387         metadata_path = cache_find_location(CACHE_TYPE_XMP_METADATA, fd->path);
388         if (metadata_path && (!except || strcmp(metadata_path, except) != 0)) 
389                 {
390                 metadata_pathl = path_from_utf8(metadata_path);
391                 unlink(metadata_pathl);
392                 g_free(metadata_pathl);
393                 g_free(metadata_path);
394                 }
395 }
396
397 static gint metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
398 {
399         gchar *metadata_path;
400         gchar *metadata_pathl;
401         gint success = FALSE;
402         if (!fd) return FALSE;
403
404         metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
405         if (!metadata_path) return FALSE;
406
407         metadata_pathl = path_from_utf8(metadata_path);
408
409         success = metadata_file_read(metadata_pathl, keywords, comment);
410
411         g_free(metadata_pathl);
412         g_free(metadata_path);
413
414         return success;
415 }
416
417 static GList *remove_duplicate_strings_from_list(GList *list)
418 {
419         GList *work = list;
420         GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
421         GList *newlist = NULL;
422
423         while (work)
424                 {
425                 gchar *key = work->data;
426
427                 if (g_hash_table_lookup(hashtable, key) == NULL)
428                         {
429                         g_hash_table_insert(hashtable, (gpointer) key, GINT_TO_POINTER(1));
430                         newlist = g_list_prepend(newlist, key);
431                         }
432                 work = work->next;
433                 }
434
435         g_hash_table_destroy(hashtable);
436         g_list_free(list);
437
438         return g_list_reverse(newlist);
439 }
440
441 GList *metadata_read_list(FileData *fd, const gchar *key)
442 {
443         ExifData *exif;
444         GList *list = NULL;
445         if (!fd) return NULL;
446
447         /* unwritten data overide everything */
448         if (fd->modified_xmp)
449                 {
450                 list = g_hash_table_lookup(fd->modified_xmp, key);
451                 if (list) return string_list_copy(list);
452                 }
453
454         /* 
455             Legacy metadata file is the primary source if it exists.
456             Merging the lists does not make much sense, because the existence of
457             legacy metadata file indicates that the other metadata sources are not
458             writable and thus it would not be possible to delete the keywords
459             that comes from the image file.
460         */
461         if (strcmp(key, KEYWORD_KEY) == 0)
462                 {
463                 if (metadata_legacy_read(fd, &list, NULL)) return list;
464                 }
465
466         if (strcmp(key, COMMENT_KEY) == 0)
467                 {
468                 gchar *comment = NULL;
469                 if (metadata_legacy_read(fd, NULL, &comment)) return g_list_append(NULL, comment);
470                 }
471         
472         exif = exif_read_fd(fd); /* this is cached, thus inexpensive */
473         if (!exif) return NULL;
474         list = exif_get_metadata(exif, key);
475         exif_free_fd(fd, exif);
476         return list;
477 }
478
479 gchar *metadata_read_string(FileData *fd, const gchar *key)
480 {
481         GList *string_list = metadata_read_list(fd, key);
482         if (string_list)
483                 {
484                 gchar *str = string_list->data;
485                 string_list->data = NULL;
486                 string_list_free(string_list);
487                 return str;
488                 }
489         return NULL;
490 }
491         
492 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
493 {
494         gchar *str = metadata_read_string(fd, key);
495         
496         if (!str) 
497                 {
498                 return metadata_write_string(fd, key, value);
499                 }
500         else
501                 {
502                 gchar *new_string = g_strconcat(str, value, NULL);
503                 gboolean ret = metadata_write_string(fd, key, new_string);
504                 g_free(str);
505                 g_free(new_string);
506                 return ret;
507                 }
508 }
509
510 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
511 {
512         GList *list = metadata_read_list(fd, key);
513         
514         if (!list) 
515                 {
516                 return metadata_write_list(fd, key, values);
517                 }
518         else
519                 {
520                 gboolean ret;
521                 list = g_list_concat(list, string_list_copy(values));
522                 list = remove_duplicate_strings_from_list(list);
523                 
524                 ret = metadata_write_list(fd, key, list);
525                 string_list_free(list);
526                 return ret;
527                 }
528 }
529
530 gboolean find_string_in_list(GList *list, const gchar *string)
531 {
532         while (list)
533                 {
534                 gchar *haystack = list->data;
535
536                 if (haystack && string && strcmp(haystack, string) == 0) return TRUE;
537
538                 list = list->next;
539                 }
540
541         return FALSE;
542 }
543
544 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
545
546 GList *string_to_keywords_list(const gchar *text)
547 {
548         GList *list = NULL;
549         const gchar *ptr = text;
550
551         while (*ptr != '\0')
552                 {
553                 const gchar *begin;
554                 gint l = 0;
555
556                 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
557                 begin = ptr;
558                 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
559                         {
560                         ptr++;
561                         l++;
562                         }
563
564                 /* trim starting and ending whitespaces */
565                 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
566                 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
567
568                 if (l > 0)
569                         {
570                         gchar *keyword = g_strndup(begin, l);
571
572                         /* only add if not already in the list */
573                         if (find_string_in_list(list, keyword) == FALSE)
574                                 list = g_list_append(list, keyword);
575                         else
576                                 g_free(keyword);
577                         }
578                 }
579
580         return list;
581 }
582
583 /*
584  * keywords to marks
585  */
586  
587
588 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
589 {
590         GList *keywords;
591         gboolean found = FALSE;
592         keywords = metadata_read_list(fd, KEYWORD_KEY);
593         if (keywords)
594                 {
595                 GList *work = keywords;
596
597                 while (work)
598                         {
599                         gchar *kw = work->data;
600                         work = work->next;
601                         
602                         if (strcmp(kw, data) == 0)
603                                 {
604                                 found = TRUE;
605                                 break;
606                                 }
607                         }
608                 string_list_free(keywords);
609                 }
610         return found;
611 }
612
613 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
614 {
615         GList *keywords = NULL;
616         gboolean found = FALSE;
617         gboolean changed = FALSE;
618         GList *work;
619         keywords = metadata_read_list(fd, KEYWORD_KEY);
620
621         work = keywords;
622
623         while (work)
624                 {
625                 gchar *kw = work->data;
626                 
627                 if (strcmp(kw, data) == 0)
628                         {
629                         found = TRUE;
630                         if (!value) 
631                                 {
632                                 changed = TRUE;
633                                 keywords = g_list_delete_link(keywords, work);
634                                 g_free(kw);
635                                 }
636                         break;
637                         }
638                 work = work->next;
639                 }
640         if (value && !found) 
641                 {
642                 changed = TRUE;
643                 keywords = g_list_append(keywords, g_strdup(data));
644                 }
645         
646         if (changed) metadata_write_list(fd, KEYWORD_KEY, keywords);
647
648         string_list_free(keywords);
649         return TRUE;
650 }
651
652
653 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */