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