most of the metadata options now works
[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 #define COMMENT_KEY "Xmp.dc.description"
34 #define KEYWORD_KEY "Xmp.dc.subject"
35
36 static gboolean metadata_write_queue_idle_cb(gpointer data);
37 static gint metadata_legacy_write(FileData *fd);
38 static gint metadata_legacy_delete(FileData *fd);
39
40
41
42 gboolean metadata_can_write_directly(FileData *fd)
43 {
44         return (filter_file_class(fd->extension, FORMAT_CLASS_IMAGE));
45 /* FIXME: detect what exiv2 really supports */
46 }
47
48 gboolean metadata_can_write_sidecar(FileData *fd)
49 {
50         return (filter_file_class(fd->extension, FORMAT_CLASS_RAWIMAGE));
51 /* FIXME: detect what exiv2 really supports */
52 }
53
54
55 /*
56  *-------------------------------------------------------------------
57  * write queue
58  *-------------------------------------------------------------------
59  */
60
61 static GList *metadata_write_queue = NULL;
62 static gint metadata_write_idle_id = -1;
63
64 static FileData *metadata_xmp_sidecar_fd(FileData *fd)
65 {
66         GList *work;
67         gchar *base, *new_name;
68         FileData *ret;
69         
70         if (!metadata_can_write_sidecar(fd)) return NULL;
71                 
72         
73         if (fd->parent) fd = fd->parent;
74         
75         if (filter_file_class(fd->extension, FORMAT_CLASS_META))
76                 return file_data_ref(fd);
77         
78         work = fd->sidecar_files;
79         while (work)
80                 {
81                 FileData *sfd = work->data;
82                 work = work->next;
83                 if (filter_file_class(sfd->extension, FORMAT_CLASS_META))
84                         return file_data_ref(sfd);
85                 }
86         
87         /* sidecar does not exist yet */
88         base = remove_extension_from_path(fd->path);
89         new_name = g_strconcat(base, ".xmp", NULL);
90         g_free(base);
91         ret = file_data_new_simple(new_name);
92         g_free(new_name);
93         return ret;
94 }
95
96 static FileData *metadata_xmp_main_fd(FileData *fd)
97 {
98         if (filter_file_class(fd->extension, FORMAT_CLASS_META) && !g_list_find(metadata_write_queue, fd))
99                 {
100                 /* fd is a sidecar, we have to find the original file */
101                 
102                 GList *work = metadata_write_queue;
103                 while (work)
104                         {
105                         FileData *ofd = work->data;
106                         FileData *osfd = metadata_xmp_sidecar_fd(ofd);
107                         work = work->next;
108                         file_data_unref(osfd);
109                         if (fd == osfd)
110                                 {
111                                 return ofd; /* this is the main file */
112                                 }
113                         }
114                 }
115         return NULL;
116 }
117
118
119 static void metadata_write_queue_add(FileData *fd)
120 {
121         if (g_list_find(metadata_write_queue, fd)) return;
122         
123         metadata_write_queue = g_list_prepend(metadata_write_queue, fd);
124         file_data_ref(fd);
125
126         if (metadata_write_idle_id == -1) metadata_write_idle_id = g_idle_add(metadata_write_queue_idle_cb, NULL);
127 }
128
129
130 gboolean metadata_write_queue_remove(FileData *fd)
131 {
132         FileData *main_fd = metadata_xmp_main_fd(fd);
133
134         if (main_fd) fd = main_fd;
135
136         g_hash_table_destroy(fd->modified_xmp);
137         fd->modified_xmp = NULL;
138
139         metadata_write_queue = g_list_remove(metadata_write_queue, fd);
140         
141         file_data_increment_version(fd);
142         file_data_send_notification(fd, NOTIFY_TYPE_REREAD);
143
144         file_data_unref(fd);
145         return TRUE;
146 }
147
148 gboolean metadata_write_queue_remove_list(GList *list)
149 {
150         GList *work;
151         gboolean ret = TRUE;
152         
153         work = list;
154         while (work)
155                 {
156                 FileData *fd = work->data;
157                 work = work->next;
158                 ret = ret && metadata_write_queue_remove(fd);
159                 }
160         return ret;
161 }
162
163
164 static gboolean metadata_write_queue_idle_cb(gpointer data)
165 {
166         GList *work;
167         GList *to_approve = NULL;
168         
169         work = metadata_write_queue;
170         while (work)
171                 {
172                 FileData *fd = work->data;
173                 work = work->next;
174                 
175                 if (fd->change) continue; /* another operation in progress, skip this file for now */
176                 
177                 FileData *to_approve_fd = metadata_xmp_sidecar_fd(fd);
178                 
179                 if (!to_approve_fd) to_approve_fd = file_data_ref(fd); /* this is not a sidecar */
180
181                 to_approve = g_list_prepend(to_approve, to_approve_fd);
182                 }
183
184         file_util_write_metadata(NULL, to_approve, NULL);
185         
186         filelist_free(to_approve);
187
188         metadata_write_idle_id = -1;
189         return FALSE;
190 }
191
192 gboolean metadata_write_exif(FileData *fd, FileData *sfd)
193 {
194         gboolean success;
195         ExifData *exif;
196         
197         /*  we can either use cached metadata which have fd->modified_xmp already applied 
198                                      or read metadata from file and apply fd->modified_xmp
199             metadata are read also if the file was modified meanwhile */
200         exif = exif_read_fd(fd); 
201         if (!exif) return FALSE;
202         success = sfd ? exif_write_sidecar(exif, sfd->path) : exif_write(exif); /* write modified metadata */
203         exif_free_fd(fd, exif);
204         return success;
205 }
206
207 gboolean metadata_write_perform(FileData *fd)
208 {
209         FileData *sfd = NULL;
210         FileData *main_fd = metadata_xmp_main_fd(fd);
211
212         if (main_fd)
213                 {
214                 sfd = fd;
215                 fd = main_fd;
216                 }
217
218         if (options->metadata.save_in_image_file &&
219             metadata_write_exif(fd, sfd))
220                 {
221                 metadata_legacy_delete(fd);
222                 if (sfd) metadata_legacy_delete(sfd);
223                 }
224         else
225                 {
226                 metadata_legacy_write(fd);
227                 }
228         return TRUE;
229 }
230
231 gint metadata_write_list(FileData *fd, const gchar *key, GList *values)
232 {
233         if (!fd->modified_xmp)
234                 {
235                 fd->modified_xmp = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)string_list_free);
236                 }
237         g_hash_table_insert(fd->modified_xmp, g_strdup(key), values);
238         if (fd->exif)
239                 {
240                 exif_update_metadata(fd->exif, key, values);
241                 }
242         metadata_write_queue_add(fd);
243         return TRUE;
244 }
245         
246 gint metadata_write_string(FileData *fd, const gchar *key, const char *value)
247 {
248         return metadata_write_list(fd, key, g_list_append(NULL, g_strdup(value)));
249 }
250
251
252 /*
253  *-------------------------------------------------------------------
254  * keyword / comment read/write
255  *-------------------------------------------------------------------
256  */
257
258 static gint metadata_file_write(gchar *path, GHashTable *modified_xmp)
259 {
260         SecureSaveInfo *ssi;
261         GList *keywords = g_hash_table_lookup(modified_xmp, KEYWORD_KEY);
262         GList *comment_l = g_hash_table_lookup(modified_xmp, COMMENT_KEY);
263         gchar *comment = comment_l ? comment_l->data : NULL;
264
265         ssi = secure_open(path);
266         if (!ssi) return FALSE;
267
268         secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
269
270         secure_fprintf(ssi, "[keywords]\n");
271         while (keywords && secsave_errno == SS_ERR_NONE)
272                 {
273                 const gchar *word = keywords->data;
274                 keywords = keywords->next;
275
276                 secure_fprintf(ssi, "%s\n", word);
277                 }
278         secure_fputc(ssi, '\n');
279
280         secure_fprintf(ssi, "[comment]\n");
281         secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
282
283         secure_fprintf(ssi, "#end\n");
284
285         return (secure_close(ssi) == 0);
286 }
287
288 static gint metadata_legacy_write(FileData *fd)
289 {
290         gchar *metadata_path;
291         gint success = FALSE;
292
293         /* If an existing metadata file exists, we will try writing to
294          * it's location regardless of the user's preference.
295          */
296         metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
297         if (metadata_path && !access_file(metadata_path, W_OK))
298                 {
299                 g_free(metadata_path);
300                 metadata_path = NULL;
301                 }
302
303         if (!metadata_path)
304                 {
305                 gchar *metadata_dir;
306                 mode_t mode = 0755;
307
308                 metadata_dir = cache_get_location(CACHE_TYPE_METADATA, fd->path, FALSE, &mode);
309                 if (recursive_mkdir_if_not_exists(metadata_dir, mode))
310                         {
311                         gchar *filename = g_strconcat(fd->name, GQ_CACHE_EXT_METADATA, NULL);
312                         
313                         metadata_path = g_build_filename(metadata_dir, filename, NULL);
314                         g_free(filename);
315                         }
316                 g_free(metadata_dir);
317                 }
318
319         if (metadata_path)
320                 {
321                 gchar *metadata_pathl;
322
323                 DEBUG_1("Saving comment: %s", metadata_path);
324
325                 metadata_pathl = path_from_utf8(metadata_path);
326
327                 success = metadata_file_write(metadata_pathl, fd->modified_xmp);
328
329                 g_free(metadata_pathl);
330                 g_free(metadata_path);
331                 }
332
333         return success;
334 }
335
336 static gint metadata_file_read(gchar *path, GList **keywords, gchar **comment)
337 {
338         FILE *f;
339         gchar s_buf[1024];
340         MetadataKey key = MK_NONE;
341         GList *list = NULL;
342         GString *comment_build = NULL;
343
344         f = fopen(path, "r");
345         if (!f) return FALSE;
346
347         while (fgets(s_buf, sizeof(s_buf), f))
348                 {
349                 gchar *ptr = s_buf;
350
351                 if (*ptr == '#') continue;
352                 if (*ptr == '[' && key != MK_COMMENT)
353                         {
354                         gchar *keystr = ++ptr;
355                         
356                         key = MK_NONE;
357                         while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
358                         
359                         if (*ptr == ']')
360                                 {
361                                 *ptr = '\0';
362                                 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
363                                         key = MK_KEYWORDS;
364                                 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
365                                         key = MK_COMMENT;
366                                 }
367                         continue;
368                         }
369                 
370                 switch(key)
371                         {
372                         case MK_NONE:
373                                 break;
374                         case MK_KEYWORDS:
375                                 {
376                                 while (*ptr != '\n' && *ptr != '\0') ptr++;
377                                 *ptr = '\0';
378                                 if (strlen(s_buf) > 0)
379                                         {
380                                         gchar *kw = utf8_validate_or_convert(s_buf);
381
382                                         list = g_list_prepend(list, kw);
383                                         }
384                                 }
385                                 break;
386                         case MK_COMMENT:
387                                 if (!comment_build) comment_build = g_string_new("");
388                                 g_string_append(comment_build, s_buf);
389                                 break;
390                         }
391                 }
392         
393         fclose(f);
394
395         *keywords = g_list_reverse(list);
396         if (comment_build)
397                 {
398                 if (comment)
399                         {
400                         gint len;
401                         gchar *ptr = comment_build->str;
402
403                         /* strip leading and trailing newlines */
404                         while (*ptr == '\n') ptr++;
405                         len = strlen(ptr);
406                         while (len > 0 && ptr[len - 1] == '\n') len--;
407                         if (ptr[len] == '\n') len++; /* keep the last one */
408                         if (len > 0)
409                                 {
410                                 gchar *text = g_strndup(ptr, len);
411
412                                 *comment = utf8_validate_or_convert(text);
413                                 g_free(text);
414                                 }
415                         }
416                 g_string_free(comment_build, TRUE);
417                 }
418
419         return TRUE;
420 }
421
422 static gint metadata_legacy_delete(FileData *fd)
423 {
424         gchar *metadata_path;
425         gchar *metadata_pathl;
426         gint success = FALSE;
427         if (!fd) return FALSE;
428
429         metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
430         if (!metadata_path) return FALSE;
431
432         metadata_pathl = path_from_utf8(metadata_path);
433
434         success = !unlink(metadata_pathl);
435
436         g_free(metadata_pathl);
437         g_free(metadata_path);
438
439         return success;
440 }
441
442 static gint metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
443 {
444         gchar *metadata_path;
445         gchar *metadata_pathl;
446         gint success = FALSE;
447         if (!fd) return FALSE;
448
449         metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
450         if (!metadata_path) return FALSE;
451
452         metadata_pathl = path_from_utf8(metadata_path);
453
454         success = metadata_file_read(metadata_pathl, keywords, comment);
455
456         g_free(metadata_pathl);
457         g_free(metadata_path);
458
459         return success;
460 }
461
462 static GList *remove_duplicate_strings_from_list(GList *list)
463 {
464         GList *work = list;
465         GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
466         GList *newlist = NULL;
467
468         while (work)
469                 {
470                 gchar *key = work->data;
471
472                 if (g_hash_table_lookup(hashtable, key) == NULL)
473                         {
474                         g_hash_table_insert(hashtable, (gpointer) key, GINT_TO_POINTER(1));
475                         newlist = g_list_prepend(newlist, key);
476                         }
477                 work = work->next;
478                 }
479
480         g_hash_table_destroy(hashtable);
481         g_list_free(list);
482
483         return g_list_reverse(newlist);
484 }
485
486
487 static gint metadata_xmp_read(FileData *fd, GList **keywords, gchar **comment)
488 {
489         ExifData *exif;
490
491         exif = exif_read_fd(fd);
492         if (!exif) return FALSE;
493
494         if (comment)
495                 {
496                 gchar *text;
497                 ExifItem *item = exif_get_item(exif, COMMENT_KEY);
498
499                 text = exif_item_get_string(item, 0);
500                 *comment = utf8_validate_or_convert(text);
501                 g_free(text);
502                 }
503
504         if (keywords)
505                 {
506                 ExifItem *item;
507                 guint i;
508                 
509                 *keywords = NULL;
510                 item = exif_get_item(exif, KEYWORD_KEY);
511                 for (i = 0; i < exif_item_get_elements(item); i++)
512                         {
513                         gchar *kw = exif_item_get_string(item, i);
514                         gchar *utf8_kw;
515
516                         if (!kw) break;
517
518                         utf8_kw = utf8_validate_or_convert(kw);
519                         *keywords = g_list_append(*keywords, (gpointer) utf8_kw);
520                         g_free(kw);
521                         }
522
523                 /* FIXME:
524                  * Exiv2 handles Iptc keywords as multiple entries with the
525                  * same key, thus exif_get_item returns only the first keyword
526                  * and the only way to get all keywords is to iterate through
527                  * the item list.
528                  */
529                 for (item = exif_get_first_item(exif);
530                      item;
531                      item = exif_get_next_item(exif))
532                         {
533                         guint tag;
534                 
535                         tag = exif_item_get_tag_id(item);
536                         if (tag == 0x0019)
537                                 {
538                                 gchar *tag_name = exif_item_get_tag_name(item);
539
540                                 if (strcmp(tag_name, "Iptc.Application2.Keywords") == 0)
541                                         {
542                                         gchar *kw;
543                                         gchar *utf8_kw;
544
545                                         kw = exif_item_get_data_as_text(item);
546                                         if (!kw) continue;
547
548                                         utf8_kw = utf8_validate_or_convert(kw);
549                                         *keywords = g_list_append(*keywords, (gpointer) utf8_kw);
550                                         g_free(kw);
551                                         }
552                                 g_free(tag_name);
553                                 }
554                         }
555                 }
556
557         exif_free_fd(fd, exif);
558
559         return (comment && *comment) || (keywords && *keywords);
560 }
561
562 gint metadata_write(FileData *fd, GList *keywords, const gchar *comment)
563 {
564         gint success = TRUE;
565         gint write_comment = (comment && comment[0]);
566
567         if (!fd) return FALSE;
568
569         if (write_comment) success = success && metadata_write_string(fd, COMMENT_KEY, comment);
570         if (keywords) success = success && metadata_write_list(fd, KEYWORD_KEY, string_list_copy(keywords));
571         
572         if (options->metadata.sync_grouped_files)
573                 {
574                 GList *work = fd->sidecar_files;
575                 
576                 while (work)
577                         {
578                         FileData *sfd = work->data;
579                         work = work->next;
580                         
581                         if (filter_file_class(sfd->extension, FORMAT_CLASS_META)) continue; 
582
583                         if (write_comment) success = success && metadata_write_string(sfd, COMMENT_KEY, comment);
584                         if (keywords) success = success && metadata_write_list(sfd, KEYWORD_KEY, string_list_copy(keywords));
585                         }
586                 }
587
588         return success;
589 }
590
591 gint metadata_read(FileData *fd, GList **keywords, gchar **comment)
592 {
593         GList *keywords_xmp = NULL;
594         GList *keywords_legacy = NULL;
595         gchar *comment_xmp = NULL;
596         gchar *comment_legacy = NULL;
597         gint result_xmp, result_legacy;
598
599         if (!fd) return FALSE;
600
601         result_xmp = metadata_xmp_read(fd, &keywords_xmp, &comment_xmp);
602         result_legacy = metadata_legacy_read(fd, &keywords_legacy, &comment_legacy);
603
604         if (!result_xmp && !result_legacy)
605                 {
606                 return FALSE;
607                 }
608
609         if (keywords)
610                 {
611                 if (result_xmp && result_legacy)
612                         *keywords = g_list_concat(keywords_xmp, keywords_legacy);
613                 else
614                         *keywords = result_xmp ? keywords_xmp : keywords_legacy;
615
616                 *keywords = remove_duplicate_strings_from_list(*keywords);
617                 }
618         else
619                 {
620                 if (result_xmp) string_list_free(keywords_xmp);
621                 if (result_legacy) string_list_free(keywords_legacy);
622                 }
623
624
625         if (comment)
626                 {
627                 if (result_xmp && result_legacy && comment_xmp && comment_legacy && *comment_xmp && *comment_legacy)
628                         *comment = g_strdup_printf("%s\n%s", comment_xmp, comment_legacy);
629                 else
630                         *comment = result_xmp ? comment_xmp : comment_legacy;
631                 }
632
633         if (result_xmp && (!comment || *comment != comment_xmp)) g_free(comment_xmp);
634         if (result_legacy && (!comment || *comment != comment_legacy)) g_free(comment_legacy);
635         
636         // return FALSE in the following cases:
637         //  - only looking for a comment and didn't find one
638         //  - only looking for keywords and didn't find any
639         //  - looking for either a comment or keywords, but found nothing
640         if ((!keywords && comment   && !*comment)  ||
641             (!comment  && keywords  && !*keywords) ||
642             ( comment  && !*comment &&   keywords && !*keywords))
643                 return FALSE;
644
645         return TRUE;
646 }
647
648 void metadata_set(FileData *fd, GList *new_keywords, gchar *new_comment, gboolean append)
649 {
650         gchar *comment = NULL;
651         GList *keywords = NULL;
652         GList *keywords_list = NULL;
653
654         metadata_read(fd, &keywords, &comment);
655         
656         if (new_comment)
657                 {
658                 if (append && comment && *comment)
659                         {
660                         gchar *tmp = comment;
661                                 
662                         comment = g_strconcat(tmp, new_comment, NULL);
663                         g_free(tmp);
664                         }
665                 else
666                         {
667                         g_free(comment);
668                         comment = g_strdup(new_comment);
669                         }
670                 }
671         
672         if (new_keywords)
673                 {
674                 if (append && keywords && g_list_length(keywords) > 0)
675                         {
676                         GList *work;
677
678                         work = new_keywords;
679                         while (work)
680                                 {
681                                 gchar *key;
682                                 GList *p;
683
684                                 key = work->data;
685                                 work = work->next;
686
687                                 p = keywords;
688                                 while (p && key)
689                                         {
690                                         gchar *needle = p->data;
691                                         p = p->next;
692
693                                         if (strcmp(needle, key) == 0) key = NULL;
694                                         }
695
696                                 if (key) keywords = g_list_append(keywords, g_strdup(key));
697                                 }
698                         keywords_list = keywords;
699                         }
700                 else
701                         {
702                         keywords_list = new_keywords;
703                         }
704                 }
705         
706         metadata_write(fd, keywords_list, comment);
707
708         string_list_free(keywords);
709         g_free(comment);
710 }
711
712 gboolean find_string_in_list(GList *list, const gchar *string)
713 {
714         while (list)
715                 {
716                 gchar *haystack = list->data;
717
718                 if (haystack && string && strcmp(haystack, string) == 0) return TRUE;
719
720                 list = list->next;
721                 }
722
723         return FALSE;
724 }
725
726 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
727
728 GList *string_to_keywords_list(const gchar *text)
729 {
730         GList *list = NULL;
731         const gchar *ptr = text;
732
733         while (*ptr != '\0')
734                 {
735                 const gchar *begin;
736                 gint l = 0;
737
738                 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
739                 begin = ptr;
740                 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
741                         {
742                         ptr++;
743                         l++;
744                         }
745
746                 /* trim starting and ending whitespaces */
747                 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
748                 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
749
750                 if (l > 0)
751                         {
752                         gchar *keyword = g_strndup(begin, l);
753
754                         /* only add if not already in the list */
755                         if (find_string_in_list(list, keyword) == FALSE)
756                                 list = g_list_append(list, keyword);
757                         else
758                                 g_free(keyword);
759                         }
760                 }
761
762         return list;
763 }
764
765 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */