keyword_list_find() -> find_string_in_list(), return gboolean.
[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
26 typedef enum {
27         MK_NONE,
28         MK_KEYWORDS,
29         MK_COMMENT
30 } MetadataKey;
31
32
33 /*
34  *-------------------------------------------------------------------
35  * keyword / comment read/write
36  *-------------------------------------------------------------------
37  */
38
39 static gint metadata_file_write(gchar *path, GList *keywords, const gchar *comment)
40 {
41         SecureSaveInfo *ssi;
42
43         ssi = secure_open(path);
44         if (!ssi) return FALSE;
45
46         secure_fprintf(ssi, "#%s comment (%s)\n\n", GQ_APPNAME, VERSION);
47
48         secure_fprintf(ssi, "[keywords]\n");
49         while (keywords && secsave_errno == SS_ERR_NONE)
50                 {
51                 const gchar *word = keywords->data;
52                 keywords = keywords->next;
53
54                 secure_fprintf(ssi, "%s\n", word);
55                 }
56         secure_fputc(ssi, '\n');
57
58         secure_fprintf(ssi, "[comment]\n");
59         secure_fprintf(ssi, "%s\n", (comment) ? comment : "");
60
61         secure_fprintf(ssi, "#end\n");
62
63         return (secure_close(ssi) == 0);
64 }
65
66 static gint metadata_legacy_write(FileData *fd, GList *keywords, const gchar *comment)
67 {
68         gchar *metadata_path;
69         gint success = FALSE;
70
71         /* If an existing metadata file exists, we will try writing to
72          * it's location regardless of the user's preference.
73          */
74         metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
75         if (metadata_path && !access_file(metadata_path, W_OK))
76                 {
77                 g_free(metadata_path);
78                 metadata_path = NULL;
79                 }
80
81         if (!metadata_path)
82                 {
83                 gchar *metadata_dir;
84                 mode_t mode = 0755;
85
86                 metadata_dir = cache_get_location(CACHE_TYPE_METADATA, fd->path, FALSE, &mode);
87                 if (recursive_mkdir_if_not_exists(metadata_dir, mode))
88                         {
89                         gchar *filename = g_strconcat(fd->name, GQ_CACHE_EXT_METADATA, NULL);
90                         
91                         metadata_path = g_build_filename(metadata_dir, filename, NULL);
92                         g_free(filename);
93                         }
94                 g_free(metadata_dir);
95                 }
96
97         if (metadata_path)
98                 {
99                 gchar *metadata_pathl;
100
101                 DEBUG_1("Saving comment: %s", metadata_path);
102
103                 metadata_pathl = path_from_utf8(metadata_path);
104
105                 success = metadata_file_write(metadata_pathl, keywords, comment);
106
107                 g_free(metadata_pathl);
108                 g_free(metadata_path);
109                 }
110
111         return success;
112 }
113
114 static gint metadata_file_read(gchar *path, GList **keywords, gchar **comment)
115 {
116         FILE *f;
117         gchar s_buf[1024];
118         MetadataKey key = MK_NONE;
119         GList *list = NULL;
120         GString *comment_build = NULL;
121
122         f = fopen(path, "r");
123         if (!f) return FALSE;
124
125         while (fgets(s_buf, sizeof(s_buf), f))
126                 {
127                 gchar *ptr = s_buf;
128
129                 if (*ptr == '#') continue;
130                 if (*ptr == '[' && key != MK_COMMENT)
131                         {
132                         gchar *keystr = ++ptr;
133                         
134                         key = MK_NONE;
135                         while (*ptr != ']' && *ptr != '\n' && *ptr != '\0') ptr++;
136                         
137                         if (*ptr == ']')
138                                 {
139                                 *ptr = '\0';
140                                 if (g_ascii_strcasecmp(keystr, "keywords") == 0)
141                                         key = MK_KEYWORDS;
142                                 else if (g_ascii_strcasecmp(keystr, "comment") == 0)
143                                         key = MK_COMMENT;
144                                 }
145                         continue;
146                         }
147                 
148                 switch(key)
149                         {
150                         case MK_NONE:
151                                 break;
152                         case MK_KEYWORDS:
153                                 {
154                                 while (*ptr != '\n' && *ptr != '\0') ptr++;
155                                 *ptr = '\0';
156                                 if (strlen(s_buf) > 0)
157                                         {
158                                         gchar *kw = utf8_validate_or_convert(s_buf);
159
160                                         list = g_list_prepend(list, kw);
161                                         }
162                                 }
163                                 break;
164                         case MK_COMMENT:
165                                 if (!comment_build) comment_build = g_string_new("");
166                                 g_string_append(comment_build, s_buf);
167                                 break;
168                         }
169                 }
170         
171         fclose(f);
172
173         *keywords = g_list_reverse(list);
174         if (comment_build)
175                 {
176                 if (comment)
177                         {
178                         gint len;
179                         gchar *ptr = comment_build->str;
180
181                         /* strip leading and trailing newlines */
182                         while (*ptr == '\n') ptr++;
183                         len = strlen(ptr);
184                         while (len > 0 && ptr[len - 1] == '\n') len--;
185                         if (ptr[len] == '\n') len++; /* keep the last one */
186                         if (len > 0)
187                                 {
188                                 gchar *text = g_strndup(ptr, len);
189
190                                 *comment = utf8_validate_or_convert(text);
191                                 g_free(text);
192                                 }
193                         }
194                 g_string_free(comment_build, TRUE);
195                 }
196
197         return TRUE;
198 }
199
200 static gint metadata_legacy_delete(FileData *fd)
201 {
202         gchar *metadata_path;
203         gchar *metadata_pathl;
204         gint success = FALSE;
205         if (!fd) return FALSE;
206
207         metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
208         if (!metadata_path) return FALSE;
209
210         metadata_pathl = path_from_utf8(metadata_path);
211
212         success = !unlink(metadata_pathl);
213
214         g_free(metadata_pathl);
215         g_free(metadata_path);
216
217         return success;
218 }
219
220 static gint metadata_legacy_read(FileData *fd, GList **keywords, gchar **comment)
221 {
222         gchar *metadata_path;
223         gchar *metadata_pathl;
224         gint success = FALSE;
225         if (!fd) return FALSE;
226
227         metadata_path = cache_find_location(CACHE_TYPE_METADATA, fd->path);
228         if (!metadata_path) return FALSE;
229
230         metadata_pathl = path_from_utf8(metadata_path);
231
232         success = metadata_file_read(metadata_pathl, keywords, comment);
233
234         g_free(metadata_pathl);
235         g_free(metadata_path);
236
237         return success;
238 }
239
240 static GList *remove_duplicate_strings_from_list(GList *list)
241 {
242         GList *work = list;
243         GHashTable *hashtable = g_hash_table_new(g_str_hash, g_str_equal);
244         GList *newlist = NULL;
245
246         while (work)
247                 {
248                 gchar *key = work->data;
249
250                 if (g_hash_table_lookup(hashtable, key) == NULL)
251                         {
252                         g_hash_table_insert(hashtable, (gpointer) key, GINT_TO_POINTER(1));
253                         newlist = g_list_prepend(newlist, key);
254                         }
255                 work = work->next;
256                 }
257
258         g_hash_table_destroy(hashtable);
259         g_list_free(list);
260
261         return g_list_reverse(newlist);
262 }
263
264 #define COMMENT_KEY "Xmp.dc.description"
265 #define KEYWORD_KEY "Xmp.dc.subject"
266
267 static gint metadata_xmp_read(FileData *fd, GList **keywords, gchar **comment)
268 {
269         ExifData *exif;
270
271         exif = exif_read_fd(fd);
272         if (!exif) return FALSE;
273
274         if (comment)
275                 {
276                 gchar *text;
277                 ExifItem *item = exif_get_item(exif, COMMENT_KEY);
278
279                 text = exif_item_get_string(item, 0);
280                 *comment = utf8_validate_or_convert(text);
281                 g_free(text);
282                 }
283
284         if (keywords)
285                 {
286                 ExifItem *item;
287                 guint i;
288                 
289                 *keywords = NULL;
290                 item = exif_get_item(exif, KEYWORD_KEY);
291                 for (i = 0; i < exif_item_get_elements(item); i++)
292                         {
293                         gchar *kw = exif_item_get_string(item, i);
294                         gchar *utf8_kw;
295
296                         if (!kw) break;
297
298                         utf8_kw = utf8_validate_or_convert(kw);
299                         *keywords = g_list_append(*keywords, (gpointer) utf8_kw);
300                         g_free(kw);
301                         }
302
303                 /* FIXME:
304                  * Exiv2 handles Iptc keywords as multiple entries with the
305                  * same key, thus exif_get_item returns only the first keyword
306                  * and the only way to get all keywords is to iterate through
307                  * the item list.
308                  */
309                 for (item = exif_get_first_item(exif);
310                      item;
311                      item = exif_get_next_item(exif))
312                         {
313                         guint tag;
314                 
315                         tag = exif_item_get_tag_id(item);
316                         if (tag == 0x0019)
317                                 {
318                                 gchar *tag_name = exif_item_get_tag_name(item);
319
320                                 if (strcmp(tag_name, "Iptc.Application2.Keywords") == 0)
321                                         {
322                                         gchar *kw;
323                                         gchar *utf8_kw;
324
325                                         kw = exif_item_get_data_as_text(item);
326                                         if (!kw) continue;
327
328                                         utf8_kw = utf8_validate_or_convert(kw);
329                                         *keywords = g_list_append(*keywords, (gpointer) utf8_kw);
330                                         g_free(kw);
331                                         }
332                                 g_free(tag_name);
333                                 }
334                         }
335                 }
336
337         exif_free_fd(fd, exif);
338
339         return (comment && *comment) || (keywords && *keywords);
340 }
341
342 static gint metadata_xmp_write(FileData *fd, GList *keywords, const gchar *comment)
343 {
344         gint success;
345         gint write_comment = (comment && comment[0]);
346         ExifData *exif;
347         ExifItem *item;
348
349         exif = exif_read_fd(fd);
350         if (!exif) return FALSE;
351
352         item = exif_get_item(exif, COMMENT_KEY);
353         if (item && !write_comment)
354                 {
355                 exif_item_delete(exif, item);
356                 item = NULL;
357                 }
358
359         if (!item && write_comment) item = exif_add_item(exif, COMMENT_KEY);
360         if (item) exif_item_set_string(item, comment);
361
362         while ((item = exif_get_item(exif, KEYWORD_KEY)))
363                 {
364                 exif_item_delete(exif, item);
365                 }
366
367         if (keywords)
368                 {
369                 GList *work;
370
371                 item = exif_add_item(exif, KEYWORD_KEY);
372
373                 work = keywords;
374                 while (work)
375                         {
376                         exif_item_set_string(item, (gchar *) work->data);
377                         work = work->next;
378                         }
379                 }
380
381         success = exif_write(exif);
382
383         exif_free_fd(fd, exif);
384
385         return success;
386 }
387
388 gint metadata_write(FileData *fd, GList *keywords, const gchar *comment)
389 {
390         if (!fd) return FALSE;
391
392         if (options->save_metadata_in_image_file &&
393             metadata_xmp_write(fd, keywords, comment))
394                 {
395                 metadata_legacy_delete(fd);
396                 return TRUE;
397                 }
398
399         return metadata_legacy_write(fd, keywords, comment);
400 }
401
402 gint metadata_read(FileData *fd, GList **keywords, gchar **comment)
403 {
404         GList *keywords1 = NULL;
405         GList *keywords2 = NULL;
406         gchar *comment1 = NULL;
407         gchar *comment2 = NULL;
408         gint res1, res2;
409
410         if (!fd) return FALSE;
411
412         res1 = metadata_xmp_read(fd, &keywords1, &comment1);
413         res2 = metadata_legacy_read(fd, &keywords2, &comment2);
414
415         if (!res1 && !res2)
416                 {
417                 return FALSE;
418                 }
419
420         if (keywords)
421                 {
422                 if (res1 && res2)
423                         *keywords = g_list_concat(keywords1, keywords2);
424                 else
425                         *keywords = res1 ? keywords1 : keywords2;
426
427                 *keywords = remove_duplicate_strings_from_list(*keywords);
428                 }
429         else
430                 {
431                 if (res1) string_list_free(keywords1);
432                 if (res2) string_list_free(keywords2);
433                 }
434
435
436         if (comment)
437                 {
438                 if (res1 && res2 && comment1 && comment2 && comment1[0] && comment2[0])
439                         *comment = g_strdup_printf("%s\n%s", comment1, comment2);
440                 else
441                         *comment = res1 ? comment1 : comment2;
442                 }
443         if (res1 && (!comment || *comment != comment1)) g_free(comment1);
444         if (res2 && (!comment || *comment != comment2)) g_free(comment2);
445         
446         // return FALSE in the following cases:
447         //  - only looking for a comment and didn't find one
448         //  - only looking for keywords and didn't find any
449         //  - looking for either a comment or keywords, but found nothing
450         if ((!keywords && comment   && !*comment)  ||
451             (!comment  && keywords  && !*keywords) ||
452             ( comment  && !*comment &&   keywords && !*keywords))
453                 return FALSE;
454
455         return TRUE;
456 }
457
458 void metadata_set(FileData *fd, GList *keywords_to_use, gchar *comment_to_use, gboolean append)
459 {
460         gchar *comment = NULL;
461         GList *keywords = NULL;
462         GList *save_list = NULL;
463
464         metadata_read(fd, &keywords, &comment);
465         
466         if (comment_to_use)
467                 {
468                 if (append && comment && *comment)
469                         {
470                         gchar *tmp = comment;
471                                 
472                         comment = g_strconcat(tmp, comment_to_use, NULL);
473                         g_free(tmp);
474                         }
475                 else
476                         {
477                         g_free(comment);
478                         comment = g_strdup(comment_to_use);
479                         }
480                 }
481         
482         if (keywords_to_use)
483                 {
484                 if (append && keywords && g_list_length(keywords) > 0)
485                         {
486                         GList *work;
487
488                         work = keywords_to_use;
489                         while (work)
490                                 {
491                                 gchar *key;
492                                 GList *p;
493
494                                 key = work->data;
495                                 work = work->next;
496
497                                 p = keywords;
498                                 while (p && key)
499                                         {
500                                         gchar *needle = p->data;
501                                         p = p->next;
502
503                                         if (strcmp(needle, key) == 0) key = NULL;
504                                         }
505
506                                 if (key) keywords = g_list_append(keywords, g_strdup(key));
507                                 }
508                         save_list = keywords;
509                         }
510                 else
511                         {
512                         save_list = keywords_to_use;
513                         }
514                 }
515         
516         metadata_write(fd, save_list, comment);
517
518         string_list_free(keywords);
519         g_free(comment);
520 }
521
522 gboolean find_string_in_list(GList *list, const gchar *string)
523 {
524         while (list)
525                 {
526                 gchar *haystack = list->data;
527
528                 if (haystack && string && strcmp(haystack, string) == 0) return TRUE;
529
530                 list = list->next;
531                 }
532
533         return FALSE;
534 }
535
536 #define KEYWORDS_SEPARATOR(c) ((c) == ',' || (c) == ';' || (c) == '\n' || (c) == '\r' || (c) == '\b')
537
538 GList *string_to_keywords_list(const gchar *text)
539 {
540         GList *list = NULL;
541         const gchar *ptr = text;
542
543         while (*ptr != '\0')
544                 {
545                 const gchar *begin;
546                 gint l = 0;
547
548                 while (KEYWORDS_SEPARATOR(*ptr)) ptr++;
549                 begin = ptr;
550                 while (*ptr != '\0' && !KEYWORDS_SEPARATOR(*ptr))
551                         {
552                         ptr++;
553                         l++;
554                         }
555
556                 /* trim starting and ending whitespaces */
557                 while (l > 0 && g_ascii_isspace(*begin)) begin++, l--;
558                 while (l > 0 && g_ascii_isspace(begin[l-1])) l--;
559
560                 if (l > 0)
561                         {
562                         gchar *keyword = g_strndup(begin, l);
563
564                         /* only add if not already in the list */
565                         if (find_string_in_list(list, keyword) == FALSE)
566                                 list = g_list_append(list, keyword);
567                         else
568                                 g_free(keyword);
569                         }
570                 }
571
572         return list;
573 }
574
575 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */