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