do not ignore newly created sidecars
[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 (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)
451 {
452         ExifData *exif;
453         GList *list = NULL;
454         if (!fd) return NULL;
455
456         /* unwritten data overide everything */
457         if (fd->modified_xmp)
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);
484         exif_free_fd(fd, exif);
485         return list;
486 }
487
488 gchar *metadata_read_string(FileData *fd, const gchar *key)
489 {
490         GList *string_list = metadata_read_list(fd, key);
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 gboolean metadata_append_string(FileData *fd, const gchar *key, const char *value)
502 {
503         gchar *str = metadata_read_string(fd, key);
504         
505         if (!str) 
506                 {
507                 return metadata_write_string(fd, key, value);
508                 }
509         else
510                 {
511                 gchar *new_string = g_strconcat(str, value, NULL);
512                 gboolean ret = metadata_write_string(fd, key, new_string);
513                 g_free(str);
514                 g_free(new_string);
515                 return ret;
516                 }
517 }
518
519 gboolean metadata_append_list(FileData *fd, const gchar *key, const GList *values)
520 {
521         GList *list = metadata_read_list(fd, key);
522         
523         if (!list) 
524                 {
525                 return metadata_write_list(fd, key, values);
526                 }
527         else
528                 {
529                 gboolean ret;
530                 list = g_list_concat(list, string_list_copy(values));
531                 list = remove_duplicate_strings_from_list(list);
532                 
533                 ret = metadata_write_list(fd, key, list);
534                 string_list_free(list);
535                 return ret;
536                 }
537 }
538
539 gboolean find_string_in_list(GList *list, const gchar *string)
540 {
541         while (list)
542                 {
543                 gchar *haystack = list->data;
544
545                 if (haystack && string && strcmp(haystack, string) == 0) return TRUE;
546
547                 list = list->next;
548                 }
549
550         return FALSE;
551 }
552
553 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
554
555 GList *string_to_keywords_list(const gchar *text)
556 {
557         GList *list = NULL;
558         const gchar *ptr = text;
559
560         while (*ptr != '\0')
561                 {
562                 const gchar *begin;
563                 gint l = 0;
564
565                 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
566                 begin = ptr;
567                 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
568                         {
569                         ptr++;
570                         l++;
571                         }
572
573                 /* trim starting and ending whitespaces */
574                 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
575                 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
576
577                 if (l > 0)
578                         {
579                         gchar *keyword = g_strndup(begin, l);
580
581                         /* only add if not already in the list */
582                         if (find_string_in_list(list, keyword) == FALSE)
583                                 list = g_list_append(list, keyword);
584                         else
585                                 g_free(keyword);
586                         }
587                 }
588
589         return list;
590 }
591
592 /*
593  * keywords to marks
594  */
595  
596
597 gboolean meta_data_get_keyword_mark(FileData *fd, gint n, gpointer data)
598 {
599         GList *keywords;
600         gboolean found = FALSE;
601         keywords = metadata_read_list(fd, KEYWORD_KEY);
602         if (keywords)
603                 {
604                 GList *work = keywords;
605
606                 while (work)
607                         {
608                         gchar *kw = work->data;
609                         work = work->next;
610                         
611                         if (strcmp(kw, data) == 0)
612                                 {
613                                 found = TRUE;
614                                 break;
615                                 }
616                         }
617                 string_list_free(keywords);
618                 }
619         return found;
620 }
621
622 gboolean meta_data_set_keyword_mark(FileData *fd, gint n, gboolean value, gpointer data)
623 {
624         GList *keywords = NULL;
625         gboolean found = FALSE;
626         gboolean changed = FALSE;
627         GList *work;
628         keywords = metadata_read_list(fd, KEYWORD_KEY);
629
630         work = keywords;
631
632         while (work)
633                 {
634                 gchar *kw = work->data;
635                 
636                 if (strcmp(kw, data) == 0)
637                         {
638                         found = TRUE;
639                         if (!value) 
640                                 {
641                                 changed = TRUE;
642                                 keywords = g_list_delete_link(keywords, work);
643                                 g_free(kw);
644                                 }
645                         break;
646                         }
647                 work = work->next;
648                 }
649         if (value && !found) 
650                 {
651                 changed = TRUE;
652                 keywords = g_list_append(keywords, g_strdup(data));
653                 }
654         
655         if (changed) metadata_write_list(fd, KEYWORD_KEY, keywords);
656
657         string_list_free(keywords);
658         return TRUE;
659 }
660
661
662 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */