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